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 abstractMcpTool, theMcpToolsPool, andMcpServerConfig.@modularityjs/http-fastify-mcp-server— the Fastify driver: mounts a stateless streamable-HTTP MCP endpoint atMcpServerConfig.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:
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
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:
pools: [{ pool: McpToolsPool, key: 'create-note', useClass: CreateNoteTool }],Any MCP client then connects over streamable HTTP:
{
"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 carryAuthorization: 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=productionand noapiKey, the driver emits aModularityJsMcpServerUnauthenticatedprocess 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)
| Field | Default | Meaning |
|---|---|---|
serverName | modularityjs-app | Name reported in the initialize handshake |
serverVersion | 0.0.0 | Version reported to clients |
path | /mcp | Route the driver mounts |
apiKey | (unset — open) | Bearer token required on every request |