feathers-mcp
v3.0.0
Published
MCP implementation for FeathersJS
Maintainers
Readme
MCP implementation for FeathersJS — plugs a Model Context Protocol server into an existing FeathersJS v5 app as a regular service. Built by nesgarbo.
There is no separate process to deploy or keep in sync: feathers-mcp registers the MCP handler
as a normal Feathers custom service, so every tool call is a real, authenticated Feathers call —
real hooks, real params.user, real authorization. It serves protocol revision 2026-07-28
and the 2025-era protocol off the same endpoint, statelessly.
📖 Full documentation, in English and Spanish, lives at feathers-mcp.nesgarbo.com — architecture, a quickstart, a tool-authoring guide, the stateless request model, and the upgrade notes. This README stays intentionally shorter than the docs site; it's the "am I in the right place" overview, not the reference.
Installation
npm install feathers-mcp @modelcontextprotocol/server @modelcontextprotocol/node --saveRequires Node.js 20+ and a FeathersJS v5 app.
Integration Steps
- Configure the plugin:
In your main setup file (e.g., src/app.ts or src/app.js):
import { feathersMcp } from "feathers-mcp";
import { RepeatTextTool } from "./tools/repeat-text.tool";
app.configure(
feathersMcp({
tools: [RepeatTextTool],
})
);This registers the MCP server and your custom tools.
- Add MCP declarations:
In you src/declarations.ts file:
import type { McpToolHandler, McpServerService } from "feathers-mcp";
import { mcpServerPath } from "feathers-mcp";
export interface Configuration extends ApplicationConfiguration {
mcpToolHandler: McpToolHandler;
}
export interface ServiceTypes {
[mcpServerPath]: McpServerService;
}This ensures TypeScript recognizes mcpToolHandler and the mcp-server service.
- Setup the API Key Authentication:
You are responsible for implementing the authentication strategy and service for MCP API Keys.
Already have your own API-key/token authentication strategy registered? You don't need to
register this library's McpApiKeyStrategy at all — point feathersMcp() at your existing
strategy instead:
app.configure(
feathersMcp({
tools: [RepeatTextTool],
authStrategy: "api-key", // the name you already registered your strategy under
authField: "token", // defaults to 'apiKey' — whatever field your strategy reads
})
);allowMcpApiKey() extracts the key from the configured header and drives authenticate(authStrategy)
with { strategy: authStrategy, [authField]: key } — your existing strategy runs exactly as it
would for any other authenticated request, and this library never sees mcp-api-keys.
Don't have one yet, but want to use this library's strategy against your own key/token
service? McpApiKeyStrategy looks up the key in a service, keyed by the key itself
(.get(apiKey)), and expects a userId-like field and an isActive-like field on the record.
All three are overridable — point it at your own service instead of creating a second one:
import { McpApiKeyStrategy } from "feathers-mcp";
authentication.register(
"mcpApiKey",
new McpApiKeyStrategy({
service: "partner-tokens", // defaults to 'mcp-api-keys'
userIdField: "ownerId", // defaults to 'userId'
activeField: "enabled", // defaults to 'isActive'
})
);Whatever service you point it at just needs a get(key) that returns the matching record (or
throws NotFound) — it doesn't need to be a dedicated table; a hook-adapted view over an
existing one works too.
If you'd rather start from scratch, here's the default shape (mcp-api-keys, userId,
isActive) end to end:
- Create the mcp-api-keys service.
- Register mcpApiKey strategy in authentication.ts.
Do this:
npx feathers generate service
? What is the name of your service? mcpApiKey
? Which path should the service be registered on? mcp-api-keys
? Does this service require authentication? Yes
? What database is the service using? SQL
? Which schema definition format do you want to use? Schemas allow to type,
validate, secure and populate data TypeBox (recommended)
Updated src/client.ts
Wrote file src/services/mcp-api-keys/mcp-api-keys.schema.ts
Wrote file src/services/mcp-api-keys/mcp-api-keys.ts
Updated src/services/index.ts
Wrote file src/services/mcp-api-keys/mcp-api-keys.shared.ts
Wrote file test/services/mcp-api-keys/mcp-api-keys.test.ts
Wrote file src/services/mcp-api-keys/mcp-api-keys.class.ts
Wrote file migrations/20250528115613_mcp-api-key.tsEdit the migration
await knex.schema.createTable("mcp_api_keys", (table) => {
table.uuid("id").primary();
table
.integer("userId")
.references("id")
.inTable("users")
.onDelete("CASCADE")
.notNullable();
table.string("description").notNullable().defaultTo("");
table.boolean("isActive").notNullable().defaultTo(true);
table.timestamp("createdAt", { useTz: true });
table.timestamp("updatedAt", { useTz: true });
});Add the authStrategy in authentication.ts
import { McpApiKeyStrategy } from 'feathers-mcp'
...
authentication.register('mcpApiKey', new McpApiKeyStrategy())Add the authStrategy in default.json & production.json
"authentication": {
...
"authStrategies": [
"jwt",
"local",
"mcpApiKey"
],
...
"mcpApiKey": {
"header": "Authorization"
}
}The MCP transport writes to the raw Node socket, so feathers-mcp passes it through Feathers params.
You no longer need to declare koaRequest/koaResponse yourself — the library augments Params.
Registration is identical on Koa and Express — both are covered end-to-end by the integration tests. See the quickstart for the couple of internal details that do differ between the two.
If you use a dedicated header rather than Authorization, it carries the key bare:
"mcpApiKey": { "header": "x-api-key" }- Example Tool
Create your tools by extending BaseTool and defining input/output schemas:
import { Static, Type } from "@feathersjs/typebox";
import { McpParams, BaseTool, ToolResponse } from "feathers-mcp";
import type { EmitFunction, InferMcpToolType } from "feathers-mcp";
export const REPEAT_TEXT_TOOL_NAME = "repeat_text" as const;
export class RepeatTextTool extends BaseTool<
typeof REPEAT_TEXT_TOOL_NAME,
typeof RepeatTextTool.inputSchema,
typeof RepeatTextTool.outputSchema
> {
name = REPEAT_TEXT_TOOL_NAME;
description = "Repite un texto N veces";
// The input schema must be a Type.Object — MCP tool inputs are always objects.
static inputSchema = Type.Object({
text: Type.String({ description: "Texto a repetir" }),
times: Type.Number({ description: "Número de repeticiones" }),
});
static outputSchema = Type.String({ description: "Texto repetido" });
inputSchema = RepeatTextTool.inputSchema;
outputSchema = RepeatTextTool.outputSchema;
expose = { mcp: true, openai: true };
async handler(
{ text, times }: Static<typeof RepeatTextTool.inputSchema>,
// The authenticated Feathers params of the caller, including `params.user`.
params: McpParams,
emit: EmitFunction
) {
emit("Starting text repetition...", 0);
const result = text.repeat(times);
emit("Text repetition completed!", 100);
return { text: { type: "text", data: result } } as ToolResponse<
Static<typeof RepeatTextTool.outputSchema>
>;
}
}
declare module "feathers-mcp" {
interface McpToolMap {
[REPEAT_TEXT_TOOL_NAME]: InferMcpToolType<RepeatTextTool>;
}
}You should also augment the MCP tool types by declaring your tool.
emit sends notifications to the client while the call is still running. A bare number is progress;
pass an object for anything else:
emit("Halfway", 50); // progress notification
emit("Halfway", { progress: 50, total: 200 }); // progress out of a custom total
emit("Fetching rows", { type: "log", level: "info" }); // log notificationReturn values
A tool returns any combination of text, json, image and resource. Binary payloads are
raw base64 — no data: URI prefix:
return { image: { type: "image", data: base64, mimeType: "image/png" } };
return { json: { type: "json", result: { rows } } };Options
app.configure(
feathersMcp({
tools: [RepeatTextTool],
serverInfo: { name: "my-app", version: "2.0.0" }, // advertised to clients
})
);Debugging
Request and tool tracing is off by default. Turn it on with:
DEBUG=feathers-mcp node app.jsNotes
- Serving is stateless. Every request is authenticated on its own and answered by a fresh
McpServerwhose tool callbacks close over that request's params — there is no session to expire, cap or hijack, and no sticky sessions needed to run more than one instance. - The 2025-era
GET(standalone SSE stream) andDELETE(session termination) answer405. sessionTtlMsandmaxSessionsare accepted but do nothing; remove them.- Tool input schemas must be a
Type.Object, and two tools may not share a name — both fail at boot.
Upgrading? See CHANGELOG.md or the upgrade guide on the docs site.
License
MIT License © 2025 Nesgarbo
