@jaypie/mcp 0.8.151 → 0.8.153
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.
- package/README.md +34 -0
- package/dist/createMcpServer.d.ts +5 -0
- package/dist/createMcpServer.js +3 -2
- package/dist/createMcpServer.js.map +1 -1
- package/dist/http/index.d.ts +92 -0
- package/dist/http/index.js +3 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/mcpHttpHandler.d.ts +35 -0
- package/dist/http/mcpHttpHandler.js +99 -0
- package/dist/http/mcpHttpHandler.js.map +1 -0
- package/dist/http/rpc.d.ts +70 -0
- package/dist/http/rpc.js +262 -0
- package/dist/http/rpc.js.map +1 -0
- package/dist/http/tools.d.ts +16 -0
- package/dist/http/tools.js +23 -0
- package/dist/http/tools.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/mcpExpressHandler.d.ts +5 -0
- package/dist/mcpExpressHandler.js +2 -2
- package/dist/mcpExpressHandler.js.map +1 -1
- package/dist/suites/docs/index.js +1 -1
- package/package.json +27 -3
- package/release-notes/constructs/1.2.82.md +16 -0
- package/release-notes/datadog/1.2.8.md +19 -0
- package/release-notes/dynamodb/0.6.11.md +13 -0
- package/release-notes/express/1.2.37.md +34 -0
- package/release-notes/fabric/0.3.12.md +20 -0
- package/release-notes/jaypie/1.2.101.md +12 -0
- package/release-notes/mcp/0.8.152.md +9 -0
- package/release-notes/mcp/0.8.153.md +37 -0
- package/release-notes/testkit/1.2.72.md +19 -0
- package/release-notes/testkit/1.2.73.md +15 -0
- package/skills/aws.md +2 -1
- package/skills/cdk.md +5 -5
- package/skills/datadog.md +15 -2
- package/skills/dynamodb.md +3 -7
- package/skills/express.md +56 -2
- package/skills/fabric.md +14 -1
- package/skills/lambda.md +3 -3
- package/skills/mcp.md +79 -1
- 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
|
}
|
package/dist/createMcpServer.js
CHANGED
|
@@ -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 ${
|
|
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 ${
|
|
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 @@
|
|
|
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>;
|
package/dist/http/rpc.js
ADDED
|
@@ -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
|