Skip to content

MCP Server (App-as-MCP)

Two packages let a ModularityJS application expose its own domain operations as Model Context Protocol tools — so any MCP client (Claude Code, Claude Desktop, Cursor, an internal ops agent) can operate the running app:

  • @modularityjs/mcp-server — the contract: the abstract McpTool, the McpToolsPool, and McpServerConfig.
  • @modularityjs/http-fastify-mcp-server — the Fastify driver: mounts a stateless streamable-HTTP MCP endpoint at McpServerConfig.path (default /mcp) on the app's own HTTP port.

Not to be confused with @modularityjs/mcp, the build-time tooling server that answers questions about the framework. This pair is a runtime capability of your app.

Writing a tool

A tool is an ordinary DI-managed class — inject services, respect contracts, throw on failure:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { McpTool } from '@modularityjs/mcp-server';

@Injectable()
export class CreateNoteTool extends McpTool {
  readonly name = 'create_note';
  readonly description = 'Create a note in the current workspace';
  override readonly inputSchema = {
    type: 'object',
    properties: { title: { type: 'string' }, body: { type: 'string' } },
    required: ['title'],
  };

  constructor(@Inject(NotesService) private readonly notes: NotesService) {
    super();
  }

  async execute(args: Record<string, unknown>) {
    return this.notes.create(String(args.title), String(args.body ?? ''));
  }
}

inputSchema is plain JSON Schema — no validation-library coupling; it passes through to the client verbatim. Return values are JSON-serialized for the caller. Thrown errors become MCP tool errors (isError: true with the message), never transport failures.

Wiring

typescript
modules: [
  HttpModule.forRoot({ port: 3000 }),
  HttpFastifyModule,
  McpServerModule.forRoot({
    serverName: 'notes-app',
    apiKey: process.env.MCP_API_KEY, // see Security below
  }),
  HttpFastifyMcpServerModule,
  NotesModule, // contributes CreateNoteTool to McpToolsPool
],

…and in the feature module:

typescript
pools: [{ pool: McpToolsPool, key: 'create-note', useClass: CreateNoteTool }],

Any MCP client then connects over streamable HTTP:

json
{
  "mcpServers": {
    "notes-app": {
      "url": "https://your-app.example.com/mcp",
      "headers": { "Authorization": "Bearer <MCP_API_KEY>" }
    }
  }
}

Security

The endpoint executes real domain operations — treat it like any API surface:

  • Set apiKey. When configured, every request must carry Authorization: Bearer <apiKey>; anything else is rejected with 401 before any protocol handling.
  • Unset means open. Acceptable on localhost during development. When the app runs with NODE_ENV=production and no apiKey, the driver emits a ModularityJsMcpServerUnauthenticated process warning so the misconfiguration is visible in logs.
  • Tools run with the app's full DI — scope what a tool can do the same way you scope a controller.

Transport semantics

The driver runs the MCP streamable-HTTP transport in stateless mode: each POST is served by a fresh server/transport pair (the SDK-required pattern — shared instances collide on request IDs across concurrent clients), so the endpoint needs no session affinity and scales horizontally. GET/DELETE (session streams) answer 405. The tools listing is precomputed once at boot.

Config reference (McpServerConfig)

FieldDefaultMeaning
serverNamemodularityjs-appName reported in the initialize handshake
serverVersion0.0.0Version reported to clients
path/mcpRoute the driver mounts
apiKey(unset — open)Bearer token required on every request