@jaypie/mcp 0.8.152 → 0.8.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +34 -0
  2. package/dist/createMcpServer.d.ts +5 -0
  3. package/dist/createMcpServer.js +3 -2
  4. package/dist/createMcpServer.js.map +1 -1
  5. package/dist/http/index.d.ts +92 -0
  6. package/dist/http/index.js +3 -0
  7. package/dist/http/index.js.map +1 -0
  8. package/dist/http/mcpHttpHandler.d.ts +35 -0
  9. package/dist/http/mcpHttpHandler.js +99 -0
  10. package/dist/http/mcpHttpHandler.js.map +1 -0
  11. package/dist/http/rpc.d.ts +70 -0
  12. package/dist/http/rpc.js +262 -0
  13. package/dist/http/rpc.js.map +1 -0
  14. package/dist/http/tools.d.ts +16 -0
  15. package/dist/http/tools.js +23 -0
  16. package/dist/http/tools.js.map +1 -0
  17. package/dist/index.d.ts +10 -0
  18. package/dist/mcpExpressHandler.d.ts +5 -0
  19. package/dist/mcpExpressHandler.js +2 -2
  20. package/dist/mcpExpressHandler.js.map +1 -1
  21. package/dist/suites/docs/index.js +1 -1
  22. package/package.json +27 -3
  23. package/release-notes/constructs/1.2.82.md +16 -0
  24. package/release-notes/constructs/1.2.83.md +21 -0
  25. package/release-notes/datadog/1.2.8.md +19 -0
  26. package/release-notes/dynamodb/0.6.11.md +13 -0
  27. package/release-notes/express/1.2.37.md +34 -0
  28. package/release-notes/fabric/0.3.12.md +20 -0
  29. package/release-notes/jaypie/1.2.101.md +12 -0
  30. package/release-notes/mcp/0.8.153.md +37 -0
  31. package/release-notes/mcp/0.8.154.md +11 -0
  32. package/release-notes/testkit/1.2.73.md +15 -0
  33. package/skills/apigateway.md +1 -1
  34. package/skills/aws.md +2 -1
  35. package/skills/cdk.md +5 -5
  36. package/skills/datadog.md +15 -2
  37. package/skills/dynamodb.md +3 -7
  38. package/skills/express.md +56 -2
  39. package/skills/fabric.md +14 -1
  40. package/skills/lambda.md +3 -3
  41. package/skills/mcp.md +79 -1
  42. package/skills/streaming.md +3 -1
package/README.md CHANGED
@@ -4,6 +4,40 @@ Model Context Protocol server for Jaypie.
4
4
 
5
5
  See [jaypie.net](https://jaypie.net) for documentation.
6
6
 
7
+ ## Usage
8
+
9
+ ### CLI (stdio)
10
+
11
+ ```bash
12
+ npx jaypie-mcp
13
+ ```
14
+
15
+ ### Streamable HTTP on Lambda (no MCP SDK)
16
+
17
+ ```typescript
18
+ import express from "express";
19
+ import { createLambdaStreamHandler } from "@jaypie/express";
20
+ import { mcpHttpHandler } from "@jaypie/mcp/http";
21
+
22
+ const app = express();
23
+ app.use(express.json());
24
+ app.all("/mcp", mcpHttpHandler({ services: ["skill", "version"] }));
25
+
26
+ export const handler = createLambdaStreamHandler(app);
27
+ ```
28
+
29
+ `mcpHttpHandler` answers JSON or server-sent events by `Accept` header, 202 for notifications, and 405 for GET and DELETE. Pass `suite` to serve any fabric `ServiceSuite`. Lifecycle options (`secrets`, `setup`, `validate`, ...) pass through to `expressHandler`. Requires `@jaypie/express` and `@jaypie/logger` (optional peers, included in `jaypie`).
30
+
31
+ ### Filtering Tools
32
+
33
+ `createMcpServer`, `mcpExpressHandler`, and `mcpHttpHandler` accept `services`, an allowlist of tool names:
34
+
35
+ ```typescript
36
+ import { createMcpServer } from "@jaypie/mcp";
37
+
38
+ const server = createMcpServer({ services: ["skill", "version"] });
39
+ ```
40
+
7
41
  ## 📜 License
8
42
 
9
43
  [MIT License](./LICENSE.txt). Published by Finlayson Studio.
@@ -1,5 +1,10 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  export interface CreateMcpServerOptions {
3
+ /**
4
+ * Allowlist of tool names (service aliases) to expose. Omit to expose every
5
+ * tool; an empty array exposes none.
6
+ */
7
+ services?: string[];
3
8
  version?: string;
4
9
  verbose?: boolean;
5
10
  }
@@ -15,16 +15,17 @@ import { suite } from './suite.js';
15
15
  function createMcpServer(options = {}) {
16
16
  // Support legacy signature: createMcpServer(version: string)
17
17
  const config = typeof options === "string" ? { version: options } : options;
18
- const { version = "0.0.0", verbose = false } = config;
18
+ const { services, version = "0.0.0", verbose = false } = config;
19
19
  if (verbose) {
20
20
  console.error("[jaypie-mcp] Creating MCP server instance from suite");
21
21
  }
22
22
  const server = createMcpServerFromSuite(suite, {
23
23
  name: suite.name,
24
+ services,
24
25
  version,
25
26
  });
26
27
  if (verbose) {
27
- console.error(`[jaypie-mcp] Registered ${suite.services.length} tools from suite`);
28
+ console.error(`[jaypie-mcp] Registered ${server.tools.length} tools from suite`);
28
29
  console.error(`[jaypie-mcp] Categories: ${suite.categories.join(", ")}`);
29
30
  }
30
31
  return server;
@@ -1 +1 @@
1
- {"version":3,"file":"createMcpServer.js","sources":["../src/createMcpServer.ts"],"sourcesContent":["/* eslint-disable no-console -- MCP stdio: stderr is the only valid log channel; stdout is reserved for JSON-RPC */\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { createMcpServerFromSuite } from \"@jaypie/fabric/mcp\";\n\nimport { suite } from \"./suite.js\";\n\nexport interface CreateMcpServerOptions {\n version?: string;\n verbose?: boolean;\n}\n\n/**\n * Creates and configures an MCP server instance with Jaypie tools\n *\n * Uses ServiceSuite to register all services as MCP tools automatically.\n * Services are defined in suite.ts using fabricService and registered\n * by category. The createMcpServerFromSuite bridge converts them to\n * MCP tools with proper Zod schema validation.\n *\n * @param options - Configuration options (or legacy version string)\n * @returns Configured MCP server instance\n */\nexport function createMcpServer(\n options: CreateMcpServerOptions | string = {},\n): McpServer {\n // Support legacy signature: createMcpServer(version: string)\n const config: CreateMcpServerOptions =\n typeof options === \"string\" ? { version: options } : options;\n\n const { version = \"0.0.0\", verbose = false } = config;\n\n if (verbose) {\n console.error(\"[jaypie-mcp] Creating MCP server instance from suite\");\n }\n\n const server = createMcpServerFromSuite(suite, {\n name: suite.name,\n version,\n });\n\n if (verbose) {\n console.error(\n `[jaypie-mcp] Registered ${suite.services.length} tools from suite`,\n );\n console.error(`[jaypie-mcp] Categories: ${suite.categories.join(\", \")}`);\n }\n\n return server;\n}\n"],"names":[],"mappings":";;;AAYA;;;;;;;;;;AAUG;AACG,SAAU,eAAe,CAC7B,OAAA,GAA2C,EAAE,EAAA;;AAG7C,IAAA,MAAM,MAAM,GACV,OAAO,OAAO,KAAK,QAAQ,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO;IAE9D,MAAM,EAAE,OAAO,GAAG,OAAO,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,MAAM;IAErD,IAAI,OAAO,EAAE;AACX,QAAA,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC;IACvE;AAEA,IAAA,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,EAAE;QAC7C,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO;AACR,KAAA,CAAC;IAEF,IAAI,OAAO,EAAE;QACX,OAAO,CAAC,KAAK,CACX,CAAA,wBAAA,EAA2B,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAA,iBAAA,CAAmB,CACpE;AACD,QAAA,OAAO,CAAC,KAAK,CAAC,CAAA,yBAAA,EAA4B,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAE,CAAC;IAC1E;AAEA,IAAA,OAAO,MAAM;AACf;;;;"}
1
+ {"version":3,"file":"createMcpServer.js","sources":["../src/createMcpServer.ts"],"sourcesContent":["/* eslint-disable no-console -- MCP stdio: stderr is the only valid log channel; stdout is reserved for JSON-RPC */\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { createMcpServerFromSuite } from \"@jaypie/fabric/mcp\";\n\nimport { suite } from \"./suite.js\";\n\nexport interface CreateMcpServerOptions {\n /**\n * Allowlist of tool names (service aliases) to expose. Omit to expose every\n * tool; an empty array exposes none.\n */\n services?: string[];\n version?: string;\n verbose?: boolean;\n}\n\n/**\n * Creates and configures an MCP server instance with Jaypie tools\n *\n * Uses ServiceSuite to register all services as MCP tools automatically.\n * Services are defined in suite.ts using fabricService and registered\n * by category. The createMcpServerFromSuite bridge converts them to\n * MCP tools with proper Zod schema validation.\n *\n * @param options - Configuration options (or legacy version string)\n * @returns Configured MCP server instance\n */\nexport function createMcpServer(\n options: CreateMcpServerOptions | string = {},\n): McpServer {\n // Support legacy signature: createMcpServer(version: string)\n const config: CreateMcpServerOptions =\n typeof options === \"string\" ? { version: options } : options;\n\n const { services, version = \"0.0.0\", verbose = false } = config;\n\n if (verbose) {\n console.error(\"[jaypie-mcp] Creating MCP server instance from suite\");\n }\n\n const server = createMcpServerFromSuite(suite, {\n name: suite.name,\n services,\n version,\n });\n\n if (verbose) {\n console.error(\n `[jaypie-mcp] Registered ${server.tools.length} tools from suite`,\n );\n console.error(`[jaypie-mcp] Categories: ${suite.categories.join(\", \")}`);\n }\n\n return server;\n}\n"],"names":[],"mappings":";;;AAiBA;;;;;;;;;;AAUG;AACG,SAAU,eAAe,CAC7B,OAAA,GAA2C,EAAE,EAAA;;AAG7C,IAAA,MAAM,MAAM,GACV,OAAO,OAAO,KAAK,QAAQ,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO;AAE9D,IAAA,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,MAAM;IAE/D,IAAI,OAAO,EAAE;AACX,QAAA,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC;IACvE;AAEA,IAAA,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,EAAE;QAC7C,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ;QACR,OAAO;AACR,KAAA,CAAC;IAEF,IAAI,OAAO,EAAE;QACX,OAAO,CAAC,KAAK,CACX,CAAA,wBAAA,EAA2B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAA,iBAAA,CAAmB,CAClE;AACD,QAAA,OAAO,CAAC,KAAK,CAAC,CAAA,yBAAA,EAA4B,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAE,CAAC;IAC1E;AAEA,IAAA,OAAO,MAAM;AACf;;;;"}
@@ -0,0 +1,92 @@
1
+ import { ExpressHandlerOptions } from '@jaypie/express';
2
+ import { Request, Response } from 'express';
3
+ import { ServiceSuite } from '@jaypie/fabric';
4
+
5
+ declare const JSON_RPC_ERROR: {
6
+ readonly INTERNAL: -32603;
7
+ readonly INVALID_PARAMS: -32602;
8
+ readonly INVALID_REQUEST: -32600;
9
+ readonly METHOD_NOT_FOUND: -32601;
10
+ readonly PARSE: -32700;
11
+ };
12
+ declare const JSON_RPC_VERSION = "2.0";
13
+ /** Latest protocol version; answered when a client requests an unknown one */
14
+ declare const MCP_PROTOCOL_VERSION = "2025-06-18";
15
+ declare const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
16
+ type JsonRpcId = number | string | null;
17
+ interface JsonRpcRequest {
18
+ id?: JsonRpcId;
19
+ jsonrpc?: string;
20
+ method?: string;
21
+ params?: Record<string, unknown>;
22
+ }
23
+ interface JsonRpcResponse {
24
+ error?: {
25
+ code: number;
26
+ data?: unknown;
27
+ message: string;
28
+ };
29
+ id: JsonRpcId;
30
+ jsonrpc: typeof JSON_RPC_VERSION;
31
+ result?: unknown;
32
+ }
33
+ interface McpRpcOptions {
34
+ /** Server name reported by `initialize`. Defaults to the suite name */
35
+ name?: string;
36
+ /** Allowlist of service aliases exposed as tools. Omit to expose all */
37
+ services?: string[];
38
+ /** Suite served as tools. Defaults to the Jaypie MCP suite */
39
+ suite?: ServiceSuite;
40
+ /** Server version reported by `initialize`. Defaults to the suite version */
41
+ version?: string;
42
+ }
43
+ /**
44
+ * Answer one JSON-RPC request against a suite
45
+ *
46
+ * Returns `null` for a notification, which the transport answers with 202 and
47
+ * no body.
48
+ */
49
+ declare function handleMcpRpc(request: JsonRpcRequest, options?: McpRpcOptions): Promise<JsonRpcResponse | null>;
50
+ /**
51
+ * Answer a body that may be one request or a batch
52
+ *
53
+ * Accepts a parsed body, a JSON string, or a Buffer. Returns `null` when
54
+ * nothing needs an answer.
55
+ */
56
+ declare function handleMcpRpcBody(body: unknown, options?: McpRpcOptions): Promise<JsonRpcResponse | JsonRpcResponse[] | null>;
57
+
58
+ /**
59
+ * Options for `mcpHttpHandler`
60
+ *
61
+ * Lifecycle options (`secrets`, `setup`, `validate`, ...) pass through to
62
+ * `expressHandler` and `expressStreamHandler`. `name` and `version` describe
63
+ * the MCP server reported by `initialize`.
64
+ */
65
+ interface McpHttpHandlerOptions extends Omit<ExpressHandlerOptions, "fabric" | "name">, McpRpcOptions {
66
+ }
67
+ type McpHttpHandler = (req: Request, res: Response, ...params: unknown[]) => Promise<unknown>;
68
+ /**
69
+ * Serve a fabric `ServiceSuite` as MCP streamable HTTP without the MCP SDK
70
+ *
71
+ * Stateless JSON-RPC 2.0 on a single endpoint. A POST answers with one JSON
72
+ * body, or with server-sent events when the client's `Accept` header prefers
73
+ * `text/event-stream`. Notifications answer 202 with no body. GET, DELETE, and
74
+ * other methods answer 405.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import express from "express";
79
+ * import { createLambdaStreamHandler } from "@jaypie/express";
80
+ * import { mcpHttpHandler } from "@jaypie/mcp/http";
81
+ *
82
+ * const app = express();
83
+ * app.use(express.json());
84
+ * app.all("/mcp", mcpHttpHandler({ services: ["skill", "version"] }));
85
+ *
86
+ * export const handler = createLambdaStreamHandler(app);
87
+ * ```
88
+ */
89
+ declare function mcpHttpHandler(options?: McpHttpHandlerOptions): McpHttpHandler;
90
+
91
+ export { JSON_RPC_ERROR, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, handleMcpRpc, handleMcpRpcBody, mcpHttpHandler };
92
+ export type { JsonRpcId, JsonRpcRequest, JsonRpcResponse, McpHttpHandler, McpHttpHandlerOptions, McpRpcOptions };
@@ -0,0 +1,3 @@
1
+ export { mcpHttpHandler } from './mcpHttpHandler.js';
2
+ export { JSON_RPC_ERROR, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, handleMcpRpc, handleMcpRpcBody } from './rpc.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,35 @@
1
+ import type { ExpressHandlerOptions } from "@jaypie/express";
2
+ import type { Request, Response } from "express";
3
+ import type { McpRpcOptions } from "./rpc.js";
4
+ /**
5
+ * Options for `mcpHttpHandler`
6
+ *
7
+ * Lifecycle options (`secrets`, `setup`, `validate`, ...) pass through to
8
+ * `expressHandler` and `expressStreamHandler`. `name` and `version` describe
9
+ * the MCP server reported by `initialize`.
10
+ */
11
+ export interface McpHttpHandlerOptions extends Omit<ExpressHandlerOptions, "fabric" | "name">, McpRpcOptions {
12
+ }
13
+ export type McpHttpHandler = (req: Request, res: Response, ...params: unknown[]) => Promise<unknown>;
14
+ /**
15
+ * Serve a fabric `ServiceSuite` as MCP streamable HTTP without the MCP SDK
16
+ *
17
+ * Stateless JSON-RPC 2.0 on a single endpoint. A POST answers with one JSON
18
+ * body, or with server-sent events when the client's `Accept` header prefers
19
+ * `text/event-stream`. Notifications answer 202 with no body. GET, DELETE, and
20
+ * other methods answer 405.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import express from "express";
25
+ * import { createLambdaStreamHandler } from "@jaypie/express";
26
+ * import { mcpHttpHandler } from "@jaypie/mcp/http";
27
+ *
28
+ * const app = express();
29
+ * app.use(express.json());
30
+ * app.all("/mcp", mcpHttpHandler({ services: ["skill", "version"] }));
31
+ *
32
+ * export const handler = createLambdaStreamHandler(app);
33
+ * ```
34
+ */
35
+ export declare function mcpHttpHandler(options?: McpHttpHandlerOptions): McpHttpHandler;
@@ -0,0 +1,99 @@
1
+ import { MethodNotAllowedError } from '@jaypie/errors';
2
+ import { expressHandler, expressStreamHandler } from '@jaypie/express';
3
+ import { handleMcpRpcBody, expectsResponse } from './rpc.js';
4
+
5
+ //
6
+ //
7
+ // Constants
8
+ //
9
+ const HANDLER_NAME = {
10
+ JSON: "mcpHttpHandler",
11
+ STREAM: "mcpHttpStreamHandler",
12
+ };
13
+ const HEADER_ALLOW = "Allow";
14
+ const HTTP_ACCEPTED = 202;
15
+ const HTTP_METHOD_POST = "POST";
16
+ const MEDIA_TYPE = {
17
+ EVENT_STREAM: "text/event-stream",
18
+ JSON: "application/json",
19
+ };
20
+ const MESSAGE_METHOD_NOT_ALLOWED = "This MCP endpoint is stateless and accepts POST only";
21
+ const SSE_EVENT = "message";
22
+ //
23
+ //
24
+ // Helpers
25
+ //
26
+ /** Does the client prefer an event stream over one JSON body? */
27
+ function wantsEventStream(req) {
28
+ const accept = String(req.headers.accept ?? "");
29
+ const streamIndex = accept.indexOf(MEDIA_TYPE.EVENT_STREAM);
30
+ if (streamIndex === -1) {
31
+ return false;
32
+ }
33
+ const jsonIndex = accept.indexOf(MEDIA_TYPE.JSON);
34
+ return jsonIndex === -1 || streamIndex < jsonIndex;
35
+ }
36
+ function formatSseMessage(response) {
37
+ return `event: ${SSE_EVENT}\ndata: ${JSON.stringify(response)}\n\n`;
38
+ }
39
+ //
40
+ //
41
+ // Main
42
+ //
43
+ /**
44
+ * Serve a fabric `ServiceSuite` as MCP streamable HTTP without the MCP SDK
45
+ *
46
+ * Stateless JSON-RPC 2.0 on a single endpoint. A POST answers with one JSON
47
+ * body, or with server-sent events when the client's `Accept` header prefers
48
+ * `text/event-stream`. Notifications answer 202 with no body. GET, DELETE, and
49
+ * other methods answer 405.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import express from "express";
54
+ * import { createLambdaStreamHandler } from "@jaypie/express";
55
+ * import { mcpHttpHandler } from "@jaypie/mcp/http";
56
+ *
57
+ * const app = express();
58
+ * app.use(express.json());
59
+ * app.all("/mcp", mcpHttpHandler({ services: ["skill", "version"] }));
60
+ *
61
+ * export const handler = createLambdaStreamHandler(app);
62
+ * ```
63
+ */
64
+ function mcpHttpHandler(options = {}) {
65
+ const { name, services, suite, version, ...lifecycle } = options;
66
+ const rpcOptions = { name, services, suite, version };
67
+ const jsonHandler = expressHandler(async (req, res) => {
68
+ if (req.method !== HTTP_METHOD_POST) {
69
+ res.setHeader(HEADER_ALLOW, HTTP_METHOD_POST);
70
+ throw new MethodNotAllowedError(MESSAGE_METHOD_NOT_ALLOWED);
71
+ }
72
+ const answer = await handleMcpRpcBody(req.body, rpcOptions);
73
+ if (answer === null) {
74
+ // Streamable HTTP requires 202 with no body for notifications;
75
+ // expressHandler sends an explicit status with no body on empty return
76
+ res.status(HTTP_ACCEPTED);
77
+ return undefined;
78
+ }
79
+ return answer;
80
+ }, { ...lifecycle, name: HANDLER_NAME.JSON });
81
+ const streamHandler = expressStreamHandler(async (req, res) => {
82
+ const answer = await handleMcpRpcBody(req.body, rpcOptions);
83
+ const responses = answer === null ? [] : [answer].flat();
84
+ for (const response of responses) {
85
+ res.write(formatSseMessage(response));
86
+ }
87
+ }, { ...lifecycle, format: "sse", name: HANDLER_NAME.STREAM });
88
+ return async (req, res, ...params) => {
89
+ if (req.method === HTTP_METHOD_POST &&
90
+ wantsEventStream(req) &&
91
+ expectsResponse(req.body)) {
92
+ return streamHandler(req, res, ...params);
93
+ }
94
+ return jsonHandler(req, res, ...params);
95
+ };
96
+ }
97
+
98
+ export { mcpHttpHandler };
99
+ //# sourceMappingURL=mcpHttpHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpHttpHandler.js","sources":["../../src/http/mcpHttpHandler.ts"],"sourcesContent":["import { MethodNotAllowedError } from \"@jaypie/errors\";\nimport { expressHandler, expressStreamHandler } from \"@jaypie/express\";\nimport type { ExpressHandlerOptions } from \"@jaypie/express\";\nimport type { Request, Response } from \"express\";\n\nimport { expectsResponse, handleMcpRpcBody } from \"./rpc.js\";\nimport type { JsonRpcResponse, McpRpcOptions } from \"./rpc.js\";\n\n//\n//\n// Constants\n//\n\nconst HANDLER_NAME = {\n JSON: \"mcpHttpHandler\",\n STREAM: \"mcpHttpStreamHandler\",\n} as const;\n\nconst HEADER_ALLOW = \"Allow\";\nconst HTTP_ACCEPTED = 202;\nconst HTTP_METHOD_POST = \"POST\";\n\nconst MEDIA_TYPE = {\n EVENT_STREAM: \"text/event-stream\",\n JSON: \"application/json\",\n} as const;\n\nconst MESSAGE_METHOD_NOT_ALLOWED =\n \"This MCP endpoint is stateless and accepts POST only\";\n\nconst SSE_EVENT = \"message\";\n\n//\n//\n// Types\n//\n\n/**\n * Options for `mcpHttpHandler`\n *\n * Lifecycle options (`secrets`, `setup`, `validate`, ...) pass through to\n * `expressHandler` and `expressStreamHandler`. `name` and `version` describe\n * the MCP server reported by `initialize`.\n */\nexport interface McpHttpHandlerOptions\n extends Omit<ExpressHandlerOptions, \"fabric\" | \"name\">, McpRpcOptions {}\n\nexport type McpHttpHandler = (\n req: Request,\n res: Response,\n ...params: unknown[]\n) => Promise<unknown>;\n\n//\n//\n// Helpers\n//\n\n/** Does the client prefer an event stream over one JSON body? */\nfunction wantsEventStream(req: Request): boolean {\n const accept = String(req.headers.accept ?? \"\");\n const streamIndex = accept.indexOf(MEDIA_TYPE.EVENT_STREAM);\n if (streamIndex === -1) {\n return false;\n }\n const jsonIndex = accept.indexOf(MEDIA_TYPE.JSON);\n return jsonIndex === -1 || streamIndex < jsonIndex;\n}\n\nfunction formatSseMessage(response: JsonRpcResponse): string {\n return `event: ${SSE_EVENT}\\ndata: ${JSON.stringify(response)}\\n\\n`;\n}\n\n//\n//\n// Main\n//\n\n/**\n * Serve a fabric `ServiceSuite` as MCP streamable HTTP without the MCP SDK\n *\n * Stateless JSON-RPC 2.0 on a single endpoint. A POST answers with one JSON\n * body, or with server-sent events when the client's `Accept` header prefers\n * `text/event-stream`. Notifications answer 202 with no body. GET, DELETE, and\n * other methods answer 405.\n *\n * @example\n * ```typescript\n * import express from \"express\";\n * import { createLambdaStreamHandler } from \"@jaypie/express\";\n * import { mcpHttpHandler } from \"@jaypie/mcp/http\";\n *\n * const app = express();\n * app.use(express.json());\n * app.all(\"/mcp\", mcpHttpHandler({ services: [\"skill\", \"version\"] }));\n *\n * export const handler = createLambdaStreamHandler(app);\n * ```\n */\nexport function mcpHttpHandler(\n options: McpHttpHandlerOptions = {},\n): McpHttpHandler {\n const { name, services, suite, version, ...lifecycle } = options;\n const rpcOptions: McpRpcOptions = { name, services, suite, version };\n\n const jsonHandler = expressHandler(\n async (req: Request, res: Response) => {\n if (req.method !== HTTP_METHOD_POST) {\n res.setHeader(HEADER_ALLOW, HTTP_METHOD_POST);\n throw new MethodNotAllowedError(MESSAGE_METHOD_NOT_ALLOWED);\n }\n const answer = await handleMcpRpcBody(req.body, rpcOptions);\n if (answer === null) {\n // Streamable HTTP requires 202 with no body for notifications;\n // expressHandler sends an explicit status with no body on empty return\n res.status(HTTP_ACCEPTED);\n return undefined;\n }\n return answer;\n },\n { ...lifecycle, name: HANDLER_NAME.JSON },\n );\n\n const streamHandler = expressStreamHandler(\n async (req: Request, res: Response) => {\n const answer = await handleMcpRpcBody(req.body, rpcOptions);\n const responses = answer === null ? [] : [answer].flat();\n for (const response of responses) {\n res.write(formatSseMessage(response));\n }\n },\n { ...lifecycle, format: \"sse\", name: HANDLER_NAME.STREAM },\n );\n\n return async (req: Request, res: Response, ...params: unknown[]) => {\n if (\n req.method === HTTP_METHOD_POST &&\n wantsEventStream(req) &&\n expectsResponse(req.body)\n ) {\n return streamHandler(req, res, ...params);\n }\n return jsonHandler(req, res, ...params);\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import type { ServiceSuite } from "@jaypie/fabric";
2
+ export declare const JSON_RPC_ERROR: {
3
+ readonly INTERNAL: -32603;
4
+ readonly INVALID_PARAMS: -32602;
5
+ readonly INVALID_REQUEST: -32600;
6
+ readonly METHOD_NOT_FOUND: -32601;
7
+ readonly PARSE: -32700;
8
+ };
9
+ export declare const JSON_RPC_VERSION = "2.0";
10
+ export declare const MCP_METHOD: {
11
+ readonly CALL_TOOL: "tools/call";
12
+ readonly CANCELLED: "notifications/cancelled";
13
+ readonly INITIALIZE: "initialize";
14
+ readonly INITIALIZED: "notifications/initialized";
15
+ readonly LIST_TOOLS: "tools/list";
16
+ readonly PING: "ping";
17
+ };
18
+ /** Latest protocol version; answered when a client requests an unknown one */
19
+ export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
20
+ export declare const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
21
+ export type JsonRpcId = number | string | null;
22
+ export interface JsonRpcRequest {
23
+ id?: JsonRpcId;
24
+ jsonrpc?: string;
25
+ method?: string;
26
+ params?: Record<string, unknown>;
27
+ }
28
+ export interface JsonRpcResponse {
29
+ error?: {
30
+ code: number;
31
+ data?: unknown;
32
+ message: string;
33
+ };
34
+ id: JsonRpcId;
35
+ jsonrpc: typeof JSON_RPC_VERSION;
36
+ result?: unknown;
37
+ }
38
+ export interface McpRpcOptions {
39
+ /** Server name reported by `initialize`. Defaults to the suite name */
40
+ name?: string;
41
+ /** Allowlist of service aliases exposed as tools. Omit to expose all */
42
+ services?: string[];
43
+ /** Suite served as tools. Defaults to the Jaypie MCP suite */
44
+ suite?: ServiceSuite;
45
+ /** Server version reported by `initialize`. Defaults to the suite version */
46
+ version?: string;
47
+ }
48
+ /** A notification carries no id and expects no response */
49
+ export declare function isNotification(request: unknown): boolean;
50
+ /**
51
+ * Normalize a raw body: a Buffer or string is parsed as JSON; anything else is
52
+ * taken as already parsed. Returns `undefined` when there is no parseable body.
53
+ */
54
+ export declare function parseRpcBody(body: unknown): unknown;
55
+ /** Does the body carry anything that must be answered? */
56
+ export declare function expectsResponse(body: unknown): boolean;
57
+ /**
58
+ * Answer one JSON-RPC request against a suite
59
+ *
60
+ * Returns `null` for a notification, which the transport answers with 202 and
61
+ * no body.
62
+ */
63
+ export declare function handleMcpRpc(request: JsonRpcRequest, options?: McpRpcOptions): Promise<JsonRpcResponse | null>;
64
+ /**
65
+ * Answer a body that may be one request or a batch
66
+ *
67
+ * Accepts a parsed body, a JSON string, or a Buffer. Returns `null` when
68
+ * nothing needs an answer.
69
+ */
70
+ export declare function handleMcpRpcBody(body: unknown, options?: McpRpcOptions): Promise<JsonRpcResponse | JsonRpcResponse[] | null>;
@@ -0,0 +1,262 @@
1
+ import { isJaypieError } from '@jaypie/errors';
2
+ import { inputToJsonSchema } from '@jaypie/fabric/llm';
3
+ import { log } from '@jaypie/logger';
4
+ import { selectTools, findTool } from './tools.js';
5
+
6
+ //
7
+ //
8
+ // Constants
9
+ //
10
+ const JSON_RPC_ERROR = {
11
+ INTERNAL: -32603,
12
+ INVALID_PARAMS: -32602,
13
+ INVALID_REQUEST: -32600,
14
+ METHOD_NOT_FOUND: -32601,
15
+ PARSE: -32700,
16
+ };
17
+ const JSON_RPC_VERSION = "2.0";
18
+ const MCP_METHOD = {
19
+ CALL_TOOL: "tools/call",
20
+ CANCELLED: "notifications/cancelled",
21
+ INITIALIZE: "initialize",
22
+ INITIALIZED: "notifications/initialized",
23
+ LIST_TOOLS: "tools/list",
24
+ PING: "ping",
25
+ };
26
+ /** Latest protocol version; answered when a client requests an unknown one */
27
+ const MCP_PROTOCOL_VERSION = "2025-06-18";
28
+ const MCP_SUPPORTED_PROTOCOL_VERSIONS = [
29
+ MCP_PROTOCOL_VERSION,
30
+ "2025-03-26",
31
+ "2024-11-05",
32
+ ];
33
+ const MESSAGE = {
34
+ EMPTY_BATCH: "An empty batch is not a request",
35
+ INVALID_ARGUMENTS: "Tool arguments must be an object",
36
+ INVALID_REQUEST: "The request is not a JSON-RPC request",
37
+ PARSE: "The body is not JSON-RPC",
38
+ TOOL_FAILED: "The tool call failed",
39
+ UNKNOWN_METHOD: "Unknown method",
40
+ UNKNOWN_TOOL: "Unknown tool",
41
+ };
42
+ //
43
+ //
44
+ // Helpers
45
+ //
46
+ let defaultSuite;
47
+ /** Load the Jaypie suite only when no suite is supplied */
48
+ function loadDefaultSuite() {
49
+ if (!defaultSuite) {
50
+ defaultSuite = import('../suite.js')
51
+ .then((module) => module.suite)
52
+ .catch((error) => {
53
+ defaultSuite = undefined;
54
+ throw error;
55
+ });
56
+ }
57
+ return defaultSuite;
58
+ }
59
+ function isRecord(value) {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+ function jsonRpcError({ code, id = null, message, }) {
63
+ return { error: { code, message }, id, jsonrpc: JSON_RPC_VERSION };
64
+ }
65
+ function jsonRpcResult({ id, result, }) {
66
+ return { id, jsonrpc: JSON_RPC_VERSION, result };
67
+ }
68
+ function textContent(value) {
69
+ let text;
70
+ if (value === undefined || value === null) {
71
+ text = "";
72
+ }
73
+ else if (typeof value === "string") {
74
+ text = value;
75
+ }
76
+ else {
77
+ text = JSON.stringify(value, null, 2);
78
+ }
79
+ return [{ text, type: "text" }];
80
+ }
81
+ /** A notification carries no id and expects no response */
82
+ function isNotification(request) {
83
+ return isRecord(request) && (request.id === undefined || request.id === null);
84
+ }
85
+ /**
86
+ * Normalize a raw body: a Buffer or string is parsed as JSON; anything else is
87
+ * taken as already parsed. Returns `undefined` when there is no parseable body.
88
+ */
89
+ function parseRpcBody(body) {
90
+ const raw = Buffer.isBuffer(body) ? body.toString("utf8") : body;
91
+ if (typeof raw !== "string") {
92
+ return raw;
93
+ }
94
+ try {
95
+ return JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ /** Does the body carry anything that must be answered? */
102
+ function expectsResponse(body) {
103
+ const parsed = parseRpcBody(body);
104
+ if (Array.isArray(parsed)) {
105
+ return (parsed.length === 0 || parsed.some((entry) => !isNotification(entry)));
106
+ }
107
+ return !isNotification(parsed);
108
+ }
109
+ async function callTool({ id, params, services, suite, }) {
110
+ const name = typeof params.name === "string" ? params.name : "";
111
+ const tool = findTool(name, { services, suite });
112
+ if (!tool) {
113
+ return jsonRpcError({
114
+ code: JSON_RPC_ERROR.INVALID_PARAMS,
115
+ id,
116
+ message: `${MESSAGE.UNKNOWN_TOOL}: ${name}`,
117
+ });
118
+ }
119
+ const args = params.arguments ?? {};
120
+ if (!isRecord(args)) {
121
+ return jsonRpcError({
122
+ code: JSON_RPC_ERROR.INVALID_PARAMS,
123
+ id,
124
+ message: MESSAGE.INVALID_ARGUMENTS,
125
+ });
126
+ }
127
+ try {
128
+ const result = await tool(args);
129
+ log.trace(`[mcp] Tool call complete: ${name}`);
130
+ return jsonRpcResult({ id, result: { content: textContent(result) } });
131
+ }
132
+ catch (error) {
133
+ let message = MESSAGE.TOOL_FAILED;
134
+ if (isJaypieError(error) && error instanceof Error) {
135
+ message = error.message;
136
+ log.debug(`[mcp] Tool call rejected: ${name}`);
137
+ }
138
+ else {
139
+ log.warn(`[mcp] Tool call failed: ${name}`);
140
+ }
141
+ log.var({
142
+ mcpToolError: {
143
+ message: error instanceof Error ? error.message : String(error),
144
+ tool: name,
145
+ },
146
+ });
147
+ return jsonRpcResult({
148
+ id,
149
+ result: { content: textContent(message), isError: true },
150
+ });
151
+ }
152
+ }
153
+ //
154
+ //
155
+ // Main
156
+ //
157
+ /**
158
+ * Answer one JSON-RPC request against a suite
159
+ *
160
+ * Returns `null` for a notification, which the transport answers with 202 and
161
+ * no body.
162
+ */
163
+ async function handleMcpRpc(request, options = {}) {
164
+ const id = request.id ?? null;
165
+ const notification = isNotification(request);
166
+ if (typeof request.method !== "string") {
167
+ if (notification) {
168
+ return null;
169
+ }
170
+ return jsonRpcError({
171
+ code: JSON_RPC_ERROR.INVALID_REQUEST,
172
+ id,
173
+ message: MESSAGE.INVALID_REQUEST,
174
+ });
175
+ }
176
+ const suite = options.suite ?? (await loadDefaultSuite());
177
+ const { name = suite.name, services, version = suite.version } = options;
178
+ const params = isRecord(request.params) ? request.params : {};
179
+ log.trace(`[mcp] ${request.method}`);
180
+ switch (request.method) {
181
+ case MCP_METHOD.INITIALIZE: {
182
+ const requested = params.protocolVersion;
183
+ const protocolVersion = typeof requested === "string" &&
184
+ MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
185
+ ? requested
186
+ : MCP_PROTOCOL_VERSION;
187
+ return jsonRpcResult({
188
+ id,
189
+ result: {
190
+ capabilities: { tools: {} },
191
+ protocolVersion,
192
+ serverInfo: { name, version },
193
+ },
194
+ });
195
+ }
196
+ case MCP_METHOD.CANCELLED:
197
+ case MCP_METHOD.INITIALIZED:
198
+ return null;
199
+ case MCP_METHOD.PING:
200
+ return jsonRpcResult({ id, result: {} });
201
+ case MCP_METHOD.LIST_TOOLS:
202
+ return jsonRpcResult({
203
+ id,
204
+ result: {
205
+ tools: selectTools({ services, suite }).map((tool) => ({
206
+ description: tool.description ?? "",
207
+ inputSchema: inputToJsonSchema(tool.input),
208
+ name: String(tool.alias),
209
+ })),
210
+ },
211
+ });
212
+ case MCP_METHOD.CALL_TOOL:
213
+ return callTool({ id, params, services, suite });
214
+ default:
215
+ if (notification) {
216
+ return null;
217
+ }
218
+ return jsonRpcError({
219
+ code: JSON_RPC_ERROR.METHOD_NOT_FOUND,
220
+ id,
221
+ message: `${MESSAGE.UNKNOWN_METHOD}: ${request.method}`,
222
+ });
223
+ }
224
+ }
225
+ /**
226
+ * Answer a body that may be one request or a batch
227
+ *
228
+ * Accepts a parsed body, a JSON string, or a Buffer. Returns `null` when
229
+ * nothing needs an answer.
230
+ */
231
+ async function handleMcpRpcBody(body, options = {}) {
232
+ const parsed = parseRpcBody(body);
233
+ if (parsed === undefined) {
234
+ return jsonRpcError({ code: JSON_RPC_ERROR.PARSE, message: MESSAGE.PARSE });
235
+ }
236
+ if (Array.isArray(parsed)) {
237
+ if (parsed.length === 0) {
238
+ return jsonRpcError({
239
+ code: JSON_RPC_ERROR.INVALID_REQUEST,
240
+ message: MESSAGE.EMPTY_BATCH,
241
+ });
242
+ }
243
+ const responses = await Promise.all(parsed.map((entry) => isRecord(entry)
244
+ ? handleMcpRpc(entry, options)
245
+ : jsonRpcError({
246
+ code: JSON_RPC_ERROR.INVALID_REQUEST,
247
+ message: MESSAGE.INVALID_REQUEST,
248
+ })));
249
+ const answered = responses.filter((response) => response !== null);
250
+ return answered.length > 0 ? answered : null;
251
+ }
252
+ if (!isRecord(parsed)) {
253
+ return jsonRpcError({
254
+ code: JSON_RPC_ERROR.INVALID_REQUEST,
255
+ message: MESSAGE.INVALID_REQUEST,
256
+ });
257
+ }
258
+ return handleMcpRpc(parsed, options);
259
+ }
260
+
261
+ export { JSON_RPC_ERROR, JSON_RPC_VERSION, MCP_METHOD, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, expectsResponse, handleMcpRpc, handleMcpRpcBody, isNotification, parseRpcBody };
262
+ //# sourceMappingURL=rpc.js.map