@procedures/mcp-server 0.10.0 → 0.11.0
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/CHANGELOG.md +46 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/face.d.ts +59 -0
- package/dist/face.d.ts.map +1 -0
- package/dist/face.js +60 -0
- package/dist/face.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/transport/http.d.ts +7 -1
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +20 -11
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/mcp-transport.d.ts +9 -2
- package/dist/transport/mcp-transport.d.ts.map +1 -1
- package/dist/transport/mcp-transport.js +8 -0
- package/dist/transport/mcp-transport.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/face.ts +96 -0
- package/src/index.ts +4 -0
- package/src/transport/http.ts +24 -11
- package/src/transport/mcp-transport.ts +12 -3
- package/src/types.ts +5 -0
package/src/face.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP face of a procedures host (CLI-7): Streamable HTTP at a path of a listener of the host.
|
|
3
|
+
*
|
|
4
|
+
* `mcpFace(options)` gives a face factory for `createProcedureHost({ faces: { mcp: mcpFace() } })`
|
|
5
|
+
* of `@procedures/peer`. The face mounts on the mount list of the listener, so the MCP endpoint
|
|
6
|
+
* shares the port of the other faces. Each tool call goes through the handler of the face, thus
|
|
7
|
+
* through the request tracking of the service of the host.
|
|
8
|
+
*
|
|
9
|
+
* - The tools are the procedures of the registry of the host that its policy exposes.
|
|
10
|
+
* - Without a token of its own, the face takes the token of the policy. Without origins of its
|
|
11
|
+
* own, it takes the permitted origins of the policy.
|
|
12
|
+
* - The types are structural: this package does not depend on `@procedures/peer`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
|
+
import type { HostPolicy, ProcedureRegistry, RequestHandler } from "@procedures/client";
|
|
17
|
+
import type { McpToolFilter } from "@procedures/mcp";
|
|
18
|
+
import { McpServerTransport } from "./transport/mcp-transport.js";
|
|
19
|
+
import type { McpServerTransportOptions } from "./types.js";
|
|
20
|
+
|
|
21
|
+
/** The options of `mcpFace()`: the options of an MCP transport, with the path of its endpoint. */
|
|
22
|
+
export interface McpFaceOptions extends Omit<McpServerTransportOptions, "transport" | "httpOptions" | "sdkTransport" | "stdio" | "sseOptions" | "registry"> {
|
|
23
|
+
/** The path of the endpoint. The default is `/mcp`. */
|
|
24
|
+
readonly path?: string | undefined;
|
|
25
|
+
/** The token that each request sends as `Authorization: Bearer <token>`. The default is the token of the policy. */
|
|
26
|
+
readonly token?: string | undefined;
|
|
27
|
+
/** The browser origins that can call the endpoint, besides loopback origins. The default is the list of the policy. */
|
|
28
|
+
readonly allowedOrigins?: string[] | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the MCP face needs of its host: the handler of its calls, the registry and the policy. */
|
|
32
|
+
export interface McpFaceContext {
|
|
33
|
+
readonly handler: RequestHandler;
|
|
34
|
+
readonly registry: ProcedureRegistry;
|
|
35
|
+
readonly policy: HostPolicy;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The MCP face: its request handler, its stop parts, its endpoint and its transport. */
|
|
39
|
+
export interface McpFace {
|
|
40
|
+
readonly handler: (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
41
|
+
readonly parts: {
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
stopIntake(): void;
|
|
44
|
+
drain(): Promise<void>;
|
|
45
|
+
inFlight(): number;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
readonly endpoint: { readonly type: string; readonly scheme: "http"; readonly path: string };
|
|
49
|
+
readonly transport: McpServerTransport;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This function gives the MCP face of a procedures host. The host calls the factory one time.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const host = createProcedureHost({ registry, policy, faces: { http: true, mcp: mcpFace({ serverInfo }) }, listen });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function mcpFace(options: McpFaceOptions = {}): (context: McpFaceContext) => McpFace {
|
|
61
|
+
return (context) => {
|
|
62
|
+
const { path: givenPath, token: givenToken, allowedOrigins: givenOrigins, ...common } = options;
|
|
63
|
+
const path = givenPath ?? "/mcp";
|
|
64
|
+
const token = givenToken ?? context.policy.token;
|
|
65
|
+
const policyOrigins = context.policy.browser.allowedOrigins;
|
|
66
|
+
const allowedOrigins = givenOrigins ?? (Array.isArray(policyOrigins) ? [...policyOrigins] : undefined);
|
|
67
|
+
const toolFilter: McpToolFilter = { ...common.toolFilter, ...(context.policy.expose ? { include: context.policy.expose } : {}) };
|
|
68
|
+
const transport = new McpServerTransport(context.handler, {
|
|
69
|
+
...common,
|
|
70
|
+
registry: context.registry,
|
|
71
|
+
toolFilter,
|
|
72
|
+
transport: "http",
|
|
73
|
+
httpOptions: {
|
|
74
|
+
mount: true,
|
|
75
|
+
path,
|
|
76
|
+
...(token !== undefined ? { token } : {}),
|
|
77
|
+
...(allowedOrigins ? { allowedOrigins } : {}),
|
|
78
|
+
...(context.handler.ambientCredentials ? { ambientCredentials: context.handler.ambientCredentials } : {}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
handler: (req, res) => transport.handleHttp(req, res),
|
|
83
|
+
// The tool calls go through the request tracking of the host: the face counts no work of its own.
|
|
84
|
+
// An open stream of a session ends at the close of the face.
|
|
85
|
+
parts: {
|
|
86
|
+
start: () => transport.start(),
|
|
87
|
+
stopIntake: () => undefined,
|
|
88
|
+
drain: async () => undefined,
|
|
89
|
+
inFlight: () => 0,
|
|
90
|
+
close: () => transport.stop(),
|
|
91
|
+
},
|
|
92
|
+
endpoint: { type: "mcp", scheme: "http", path },
|
|
93
|
+
transport,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ export { createMcpHost, mcpExposeRule, redirectConsoleToStderr } from "./host.js
|
|
|
20
20
|
export type { McpHost, McpHostOptions, McpHostFaces, McpStdioFace, McpHostServerOptions, McpHostParts, McpHostState } from "./host.js";
|
|
21
21
|
export { DEFAULT_ELICIT_TIMEOUT_MS } from "./transport/mcp-transport.js";
|
|
22
22
|
|
|
23
|
+
// The MCP face of a procedures host (CLI-7): Streamable HTTP on a listener of the host
|
|
24
|
+
export { mcpFace } from "./face.js";
|
|
25
|
+
export type { McpFace, McpFaceContext, McpFaceOptions } from "./face.js";
|
|
26
|
+
|
|
23
27
|
// The envelopes of the text from outside the host (CLI-10)
|
|
24
28
|
export { createUntrustedEnvelopes, defaultUntrustedNotice, DEFAULT_UNTRUSTED_TAG } from "./untrusted.js";
|
|
25
29
|
export type { UntrustedContentOptions, UntrustedWrapContext, UntrustedEnvelopes } from "./untrusted.js";
|
package/src/transport/http.ts
CHANGED
|
@@ -23,8 +23,13 @@ import type { HttpTransportOptions } from "../types.js";
|
|
|
23
23
|
|
|
24
24
|
/** The HTTP endpoint of an MCP server, with a session for each client. */
|
|
25
25
|
export interface McpHttpTransport {
|
|
26
|
-
/** This function starts to listen, when the endpoint has a server of its own. It gives the URL. */
|
|
26
|
+
/** This function starts to listen, when the endpoint has a server of its own. It gives the URL, or the path of a mounted endpoint. */
|
|
27
27
|
listen(): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* This function answers a request of the path of the endpoint and gives true. It gives false for
|
|
30
|
+
* another path, when the endpoint does not own its server.
|
|
31
|
+
*/
|
|
32
|
+
handle(req: IncomingMessage, res: ServerResponse): boolean;
|
|
28
33
|
/** The number of open sessions. */
|
|
29
34
|
readonly sessions: number;
|
|
30
35
|
/** This function closes each session, and the server when the endpoint made one. */
|
|
@@ -41,8 +46,9 @@ export function createHttpTransport(
|
|
|
41
46
|
): McpHttpTransport {
|
|
42
47
|
const path = options.path ?? "/mcp";
|
|
43
48
|
const host = options.host ?? "127.0.0.1";
|
|
44
|
-
const
|
|
45
|
-
const
|
|
49
|
+
const mounted = options.mount === true;
|
|
50
|
+
const owned = !mounted && options.server === undefined;
|
|
51
|
+
const server: HttpServer | undefined = mounted ? undefined : (options.server ?? createServer());
|
|
46
52
|
const sessions = new Map<string, StreamableHTTPServerTransport>();
|
|
47
53
|
|
|
48
54
|
const refuse = (res: ServerResponse, status: number, code: string, message: string): void => {
|
|
@@ -69,11 +75,12 @@ export function createHttpTransport(
|
|
|
69
75
|
return transport;
|
|
70
76
|
};
|
|
71
77
|
|
|
72
|
-
const
|
|
78
|
+
const handle = (req: IncomingMessage, res: ServerResponse): boolean => {
|
|
73
79
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
74
80
|
if (url.pathname !== path) {
|
|
75
|
-
if (owned)
|
|
76
|
-
|
|
81
|
+
if (!owned) return false;
|
|
82
|
+
refuse(res, 404, "NOT_FOUND", `No MCP endpoint at ${url.pathname}`);
|
|
83
|
+
return true;
|
|
77
84
|
}
|
|
78
85
|
// A web page must not reach the tools: the host and the origin of the request. A server that
|
|
79
86
|
// reads cookies trusts a request with ambient credentials only from a named origin (CSRF)
|
|
@@ -81,11 +88,11 @@ export function createHttpTransport(
|
|
|
81
88
|
const guard = checkBrowserRequest(req, { ...(options.allowedOrigins ? { allowedOrigins: options.allowedOrigins } : {}) }, false, ambient);
|
|
82
89
|
if (!guard.ok) {
|
|
83
90
|
refuse(res, guard.status, guard.code, guard.message);
|
|
84
|
-
return;
|
|
91
|
+
return true;
|
|
85
92
|
}
|
|
86
93
|
if (options.token !== undefined && !tokenMatches(options.token, bearerOf(req.headers["authorization"]))) {
|
|
87
94
|
refuse(res, 401, "UNAUTHORIZED", "Unauthorized: this server requires its token");
|
|
88
|
-
return;
|
|
95
|
+
return true;
|
|
89
96
|
}
|
|
90
97
|
void transportOf(req)
|
|
91
98
|
.then((transport) => {
|
|
@@ -98,11 +105,17 @@ export function createHttpTransport(
|
|
|
98
105
|
.catch((error: unknown) => {
|
|
99
106
|
if (!res.headersSent) refuse(res, 500, "INTERNAL_ERROR", error instanceof Error ? error.message : String(error));
|
|
100
107
|
});
|
|
108
|
+
return true;
|
|
109
|
+
};
|
|
110
|
+
const onRequest = (req: IncomingMessage, res: ServerResponse): void => {
|
|
111
|
+
handle(req, res);
|
|
101
112
|
};
|
|
102
|
-
server
|
|
113
|
+
server?.on("request", onRequest);
|
|
103
114
|
|
|
104
115
|
return {
|
|
116
|
+
handle,
|
|
105
117
|
listen: async () => {
|
|
118
|
+
if (!server) return path;
|
|
106
119
|
if (owned && !server.listening) {
|
|
107
120
|
await new Promise<void>((resolve, reject) => {
|
|
108
121
|
server.once("error", reject);
|
|
@@ -120,11 +133,11 @@ export function createHttpTransport(
|
|
|
120
133
|
return sessions.size;
|
|
121
134
|
},
|
|
122
135
|
close: async () => {
|
|
123
|
-
server
|
|
136
|
+
server?.off("request", onRequest);
|
|
124
137
|
const open = [...sessions.values()];
|
|
125
138
|
sessions.clear();
|
|
126
139
|
await Promise.all(open.map((transport) => transport.close().catch(() => undefined)));
|
|
127
|
-
if (owned && server
|
|
140
|
+
if (owned && server?.listening) {
|
|
128
141
|
await new Promise<void>((resolve) => {
|
|
129
142
|
server.close(() => resolve());
|
|
130
143
|
server.closeAllConnections();
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
type ServerNotification,
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
import type { Transport as SdkTransport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
15
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
15
16
|
import type {
|
|
16
17
|
ServerTransport,
|
|
17
|
-
Server,
|
|
18
18
|
ProcedureRegistry,
|
|
19
19
|
RequestHandler,
|
|
20
20
|
Credentials,
|
|
@@ -156,7 +156,8 @@ export class McpServerTransport implements ServerTransport {
|
|
|
156
156
|
readonly name = "mcp";
|
|
157
157
|
|
|
158
158
|
private mcpServer: McpServer;
|
|
159
|
-
|
|
159
|
+
/** The handler of the calls: a server, or the handler of a face of a procedures host (CLI-7). */
|
|
160
|
+
private server: RequestHandler;
|
|
160
161
|
private registry: ProcedureRegistry;
|
|
161
162
|
private options: Required<Pick<McpServerTransportOptions, "transport" | "serverInfo">> &
|
|
162
163
|
McpServerTransportOptions;
|
|
@@ -176,7 +177,7 @@ export class McpServerTransport implements ServerTransport {
|
|
|
176
177
|
/** This function removes the listener of the end of the stdio input */
|
|
177
178
|
private stdinEnd: (() => void) | undefined;
|
|
178
179
|
|
|
179
|
-
constructor(server:
|
|
180
|
+
constructor(server: RequestHandler, options: McpServerTransportOptions = {}) {
|
|
180
181
|
this.server = server;
|
|
181
182
|
this.options = {
|
|
182
183
|
transport: options.transport ?? "stdio",
|
|
@@ -671,6 +672,14 @@ export class McpServerTransport implements ServerTransport {
|
|
|
671
672
|
this.log("MCP server stopped");
|
|
672
673
|
}
|
|
673
674
|
|
|
675
|
+
/**
|
|
676
|
+
* This method answers a request of the Streamable HTTP endpoint on a listener that another part
|
|
677
|
+
* owns (`httpOptions.mount`, CLI-7). It gives false for another path, and before `start()`.
|
|
678
|
+
*/
|
|
679
|
+
handleHttp(req: IncomingMessage, res: ServerResponse): boolean {
|
|
680
|
+
return this.http?.handle(req, res) ?? false;
|
|
681
|
+
}
|
|
682
|
+
|
|
674
683
|
/**
|
|
675
684
|
* Check if the transport is running.
|
|
676
685
|
*/
|
package/src/types.ts
CHANGED
|
@@ -65,6 +65,11 @@ export interface HttpTransportOptions {
|
|
|
65
65
|
allowedOrigins?: string[] | undefined;
|
|
66
66
|
/** An HTTP server to answer on, in place of a server of the transport (it does not listen then) */
|
|
67
67
|
server?: import("node:http").Server | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* True: the endpoint has no server and attaches to none. The owner of a listener mounts its
|
|
70
|
+
* request handler (`McpServerTransport.handleHttp`), for example the procedures host (CLI-7).
|
|
71
|
+
*/
|
|
72
|
+
mount?: boolean | undefined;
|
|
68
73
|
/**
|
|
69
74
|
* How the procedure server reads cookies (`ambientCredentials` of `ProcedureServer`). The
|
|
70
75
|
* transport sets it from its server. With a value, a request with a cookie needs a named origin.
|