@softure-ai/mcp-access 0.4.0 → 0.5.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 +11 -0
- package/README.md +21 -0
- package/dist/index.js +1 -1
- package/dist/server/endpoint.d.ts +13 -2
- package/dist/server/endpoint.d.ts.map +1 -1
- package/dist/server/endpoint.js +35 -2
- package/dist/server/endpoint.js.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/server-factory.d.ts +7 -3
- package/dist/server/server-factory.d.ts.map +1 -1
- package/dist/server/server-factory.js +9 -3
- package/dist/server/server-factory.js.map +1 -1
- package/dist/server/tool-list-session.d.ts +22 -0
- package/dist/server/tool-list-session.d.ts.map +1 -0
- package/dist/server/tool-list-session.js +97 -0
- package/dist/server/tool-list-session.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/sessions.d.ts +8 -0
- package/dist/testing/sessions.d.ts.map +1 -0
- package/dist/testing/sessions.js +40 -0
- package/dist/testing/sessions.js.map +1 -0
- package/module.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/server/endpoint.ts +57 -3
- package/src/server/index.ts +8 -0
- package/src/server/server-factory.ts +11 -5
- package/src/server/tool-list-session.ts +103 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/sessions.ts +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
|
|
|
4
4
|
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
5
|
"Definition of done"). Versions before the first one below are described in their GitHub Releases (`mcp-access@x.y.z`).
|
|
6
6
|
|
|
7
|
+
## 0.5.0
|
|
8
|
+
|
|
9
|
+
- Sessions that follow the tool list (#1055): `createMcpEndpoint` answers `initialize` with an `Mcp-Session-Id` that
|
|
10
|
+
carries the tool list version of the identity's access level, and answers `404 Not Found` to a request whose session
|
|
11
|
+
carries another version, so a connected client initializes again and re-lists the tools after a release that
|
|
12
|
+
changed one, without reconnecting the connector. Stateless: nothing is stored. A request without a session id is
|
|
13
|
+
served as before. `initialize` now declares `tools.listChanged: false`, since the endpoint never sends the
|
|
14
|
+
notification. `createMcpServerFactory` returns a factory with `toolListVersion(identity)`; a hand-written factory
|
|
15
|
+
passes `toolListVersion` to `createMcpEndpoint` to get sessions. `/testing` adds
|
|
16
|
+
`expectToolListSessionRenewed(before, after)`. Migrate: nothing for an app on `createMcpServerFactory`.
|
|
17
|
+
|
|
7
18
|
## 0.4.0
|
|
8
19
|
|
|
9
20
|
- Operation tools with a context getter (#990), in `@softure-ai/mcp-access/jobs`: `registerOperationStatusTool`,
|
package/README.md
CHANGED
|
@@ -189,6 +189,27 @@ export const createServer = createMcpServerFactory({
|
|
|
189
189
|
and count of each level, for an inline snapshot next to the catalog check: a schema change that forgets to update
|
|
190
190
|
the snapshot fails the test.
|
|
191
191
|
|
|
192
|
+
### Sessions that follow the tool list
|
|
193
|
+
|
|
194
|
+
A hosted client can keep its cached `tools/list` for as long as its connection lives, even after the version moved,
|
|
195
|
+
and the endpoint has no stream to send `notifications/tools/list_changed` on. So `createMcpEndpoint` ties a session to
|
|
196
|
+
the tool list (#1055):
|
|
197
|
+
|
|
198
|
+
- `initialize` answers with an `Mcp-Session-Id` that carries the tool list version of the identity's access level
|
|
199
|
+
(`<base64url(version)>.<random>`). Nothing is stored, so every instance can check it.
|
|
200
|
+
- A request whose session carries another version (a release changed a tool, or the token's access level changed)
|
|
201
|
+
answers `404 Not Found` (`-32001 Session not found`). Streamable HTTP defines that as "start a new session": the
|
|
202
|
+
client initializes again, reads the new version and lists the tools again, without the user reconnecting.
|
|
203
|
+
- A client that sends no session id is served as before. `initialize` declares `tools.listChanged: false`, since the
|
|
204
|
+
endpoint never sends the notification.
|
|
205
|
+
- The version comes from the factory: `createMcpServerFactory` returns a factory with `toolListVersion(identity)`. A
|
|
206
|
+
hand-written factory issues no session id unless the endpoint gets `toolListVersion` itself:
|
|
207
|
+
`createMcpEndpoint({ createServer, toolListVersion: async (identity) => ... })`. A failing reader logs and serves
|
|
208
|
+
the request without a session.
|
|
209
|
+
- In the app's tests, `expectToolListSessionRenewed(before, after)` (`/testing`) takes the previous and the new
|
|
210
|
+
factory and throws unless a session opened under `before` is renewed for every access level whose list changed (and
|
|
211
|
+
kept for the others).
|
|
212
|
+
|
|
192
213
|
### Strict tool input
|
|
193
214
|
|
|
194
215
|
zod objects strip unknown keys, so a call with a misspelt field (`notes` for `note`) or a field of a newer tool list
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ export const API_TOKEN_RATE_LIMIT_BUCKETS = {
|
|
|
45
45
|
export const mcpAccess = defineModule({
|
|
46
46
|
manifest: {
|
|
47
47
|
id: MODULE_ID,
|
|
48
|
-
version: "0.
|
|
48
|
+
version: "0.5.0",
|
|
49
49
|
dependsOn: { security: "^0.2.0", auth: "^0.2.0", jobs: "^0.1.1?", ops: "^0.2.0?" },
|
|
50
50
|
dbSchema: "mcp",
|
|
51
51
|
tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window", "used_confirmations", "operation_tasks"],
|
|
@@ -9,8 +9,19 @@ export declare const MCP_RATE_LIMIT_BUCKET = "mcp";
|
|
|
9
9
|
export declare const MAX_MCP_REQUEST_BYTES: number;
|
|
10
10
|
/** The app's MCP server for one request. It registers write tools only when `canWrite` is true. */
|
|
11
11
|
export type McpServerFactory = (identity: McpServerIdentity) => McpServer | Server | Promise<McpServer | Server>;
|
|
12
|
+
/** Reads the tool list version the factory's servers answer at `initialize` for the identity. */
|
|
13
|
+
export type ToolListVersionReader = (identity: McpServerIdentity) => Promise<string>;
|
|
14
|
+
/** A factory that can tell its tool list version without building a server; `createMcpServerFactory` returns one. */
|
|
15
|
+
export interface VersionedMcpServerFactory extends McpServerFactory {
|
|
16
|
+
readonly toolListVersion: ToolListVersionReader;
|
|
17
|
+
}
|
|
12
18
|
export interface McpEndpointOptions {
|
|
13
|
-
readonly createServer: McpServerFactory;
|
|
19
|
+
readonly createServer: McpServerFactory | VersionedMcpServerFactory;
|
|
20
|
+
/**
|
|
21
|
+
* The tool list version per identity, for sessions tied to it (#1055). Default: the factory's own
|
|
22
|
+
* `toolListVersion` (`createMcpServerFactory`). Without either, the endpoint issues no session id.
|
|
23
|
+
*/
|
|
24
|
+
readonly toolListVersion?: ToolListVersionReader;
|
|
14
25
|
}
|
|
15
26
|
/** Serves one request to the MCP endpoint. */
|
|
16
27
|
export type McpEndpoint = (ctx: McpAccessContext, request: Request) => Promise<Response>;
|
|
@@ -20,5 +31,5 @@ export type McpEndpoint = (ctx: McpAccessContext, request: Request) => Promise<R
|
|
|
20
31
|
* (`responseMode: "json"`): a stream would add proxy buffering and timeouts for no tool that needs
|
|
21
32
|
* it, and mid-call notifications are dropped.
|
|
22
33
|
*/
|
|
23
|
-
export declare function createMcpEndpoint({ createServer }: McpEndpointOptions): McpEndpoint;
|
|
34
|
+
export declare function createMcpEndpoint({ createServer, toolListVersion }: McpEndpointOptions): McpEndpoint;
|
|
24
35
|
//# sourceMappingURL=endpoint.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../../src/server/endpoint.ts"],"names":[],"mappings":"AAKA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,MAAM,EACZ,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGxD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../../src/server/endpoint.ts"],"names":[],"mappings":"AAKA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,MAAM,EACZ,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGxD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAWvE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC;AAE3C,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAE3C,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,QAAc,CAAC;AAOjD,mGAAmG;AACnG,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,iBAAiB,KAAK,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC;AAEjH,iGAAiG;AACjG,MAAM,MAAM,qBAAqB,GAAG,CAAC,QAAQ,EAAE,iBAAiB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAErF,qHAAqH;AACrH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;CACjD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,GAAG,yBAAyB,CAAC;IACpE;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,qBAAqB,CAAC;CAClD;AAED,8CAA8C;AAC9C,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEzF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,eAAe,EAAE,EAAE,kBAAkB,GAAG,WAAW,CA+DpG"}
|
package/dist/server/endpoint.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getProtectedResourceMetadataUrl, isOAuthEnabled } from "./oauth-http.js
|
|
|
10
10
|
import { resolveMcpOrigins } from "./origins.js";
|
|
11
11
|
import { MCP_READ_SCOPE, MCP_WRITE_SCOPE } from "./scopes.js";
|
|
12
12
|
import { verifyAccessToken } from "./tokens.js";
|
|
13
|
+
import { createSessionRenewalResponse, createToolListSessionId, isInitializeRequest, isToolListSessionCurrent, MCP_SESSION_ID_HEADER, rewriteInitializeAnswer, } from "./tool-list-session.js";
|
|
13
14
|
import { isWritingEnabled } from "./writes-switch.js";
|
|
14
15
|
export { MCP_READ_SCOPE, MCP_WRITE_SCOPE };
|
|
15
16
|
/** The security bucket the endpoint counts in; `MCP_RATE_LIMIT_BUCKETS` holds its default. */
|
|
@@ -26,7 +27,8 @@ const NO_STORE = { "cache-control": "no-store" };
|
|
|
26
27
|
* (`responseMode: "json"`): a stream would add proxy buffering and timeouts for no tool that needs
|
|
27
28
|
* it, and mid-call notifications are dropped.
|
|
28
29
|
*/
|
|
29
|
-
export function createMcpEndpoint({ createServer }) {
|
|
30
|
+
export function createMcpEndpoint({ createServer, toolListVersion }) {
|
|
31
|
+
const readToolListVersion = toolListVersion ?? getFactoryToolListVersion(createServer);
|
|
30
32
|
const handler = createMcpHandler((requestContext) => createServer(readIdentity(requestContext.authInfo)), {
|
|
31
33
|
responseMode: "json",
|
|
32
34
|
maxRequestBodySize: MAX_MCP_REQUEST_BYTES,
|
|
@@ -53,8 +55,39 @@ export function createMcpEndpoint({ createServer }) {
|
|
|
53
55
|
const authInfo = await authenticate(ctx, request);
|
|
54
56
|
if (authInfo instanceof Response)
|
|
55
57
|
return authInfo;
|
|
56
|
-
return
|
|
58
|
+
return serveInSession(request, authInfo);
|
|
57
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Sessions tied to the tool list version (#1055): a session from another version (a release changed a tool, or the
|
|
62
|
+
* access level changed) answers 404, so the client initializes again and lists the tools again; `initialize` gets a
|
|
63
|
+
* new session id. A request without a session id is served as it always was.
|
|
64
|
+
*/
|
|
65
|
+
async function serveInSession(request, authInfo) {
|
|
66
|
+
const sessionId = request.headers.get(MCP_SESSION_ID_HEADER);
|
|
67
|
+
const isInitialize = sessionId === null && (await isInitializeRequest(request, MAX_MCP_REQUEST_BYTES));
|
|
68
|
+
if (readToolListVersion === undefined || (sessionId === null && !isInitialize)) {
|
|
69
|
+
const response = await handler.fetch(request, { authInfo });
|
|
70
|
+
return isInitialize ? rewriteInitializeAnswer(response, null) : response;
|
|
71
|
+
}
|
|
72
|
+
let version;
|
|
73
|
+
try {
|
|
74
|
+
version = await readToolListVersion(readIdentity(authInfo));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Without the version no session can be checked or issued: serve the request statelessly, as before #1055.
|
|
78
|
+
console.error(`@softure-ai/mcp-access: reading the tool list version failed: ${errorLogLabel(error)}`);
|
|
79
|
+
const response = await handler.fetch(request, { authInfo });
|
|
80
|
+
return isInitialize ? rewriteInitializeAnswer(response, null) : response;
|
|
81
|
+
}
|
|
82
|
+
if (sessionId !== null) {
|
|
83
|
+
return isToolListSessionCurrent(sessionId, version) ? handler.fetch(request, { authInfo }) : createSessionRenewalResponse();
|
|
84
|
+
}
|
|
85
|
+
return rewriteInitializeAnswer(await handler.fetch(request, { authInfo }), createToolListSessionId(version));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The factory's own version reader, when it has one (`createMcpServerFactory`). */
|
|
89
|
+
function getFactoryToolListVersion(factory) {
|
|
90
|
+
return "toolListVersion" in factory && typeof factory.toolListVersion === "function" ? factory.toolListVersion : undefined;
|
|
58
91
|
}
|
|
59
92
|
/**
|
|
60
93
|
* The verified identity as the SDK's `AuthInfo`, or the challenge response. Unknown, revoked,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../src/server/endpoint.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,oGAAoG;AACpG,gGAAgG;AAChG,kGAAkG;AAClG,4BAA4B;AAC5B,OAAO,EACL,2BAA2B,EAC3B,gBAAgB,EAChB,UAAU,EACV,cAAc,GAIf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE/E,OAAO,EAAE,+BAA+B,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAyB,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC;AAE3C,8FAA8F;AAC9F,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAE3C,6FAA6F;AAC7F,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,4EAA4E;AAC5E,MAAM,aAAa,GAAG,QAAQ,CAAC;AAC/B,MAAM,aAAa,GAAG,gBAAgB,CAAC;AACvC,MAAM,QAAQ,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../src/server/endpoint.ts"],"names":[],"mappings":"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"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createMcpEndpoint, MAX_MCP_REQUEST_BYTES, MCP_RATE_LIMIT_BUCKET, MCP_READ_SCOPE, MCP_WRITE_SCOPE, type McpEndpoint, type McpEndpointOptions, type McpServerFactory, } from "./endpoint.js";
|
|
1
|
+
export { createMcpEndpoint, MAX_MCP_REQUEST_BYTES, MCP_RATE_LIMIT_BUCKET, MCP_READ_SCOPE, MCP_WRITE_SCOPE, type McpEndpoint, type McpEndpointOptions, type McpServerFactory, type ToolListVersionReader, type VersionedMcpServerFactory, } from "./endpoint.js";
|
|
2
2
|
export { actionTool, toFormData, type ActionToolOptions, type FormActionHandler } from "./action-tool.js";
|
|
3
3
|
export { CONFIRMATION_FIELD, CONFIRMATION_SECRET_ENV, confirmationInput, DEFAULT_CONFIRMATION_TTL_SECONDS, MIN_CONFIRMATION_SECRET_LENGTH, pruneUsedConfirmations, toCanonicalJson, withConfirmation, type ConfirmationErrorCode, type ConfirmationEstimate, type ConfirmationRequest, type ConfirmedToolArgs, type WithConfirmationOptions, } from "./confirmation.js";
|
|
4
4
|
export { createMcpAccessContext } from "./context.js";
|
|
@@ -21,5 +21,6 @@ export { createMcpServerFactory, type McpServerFactoryOptions } from "./server-f
|
|
|
21
21
|
export { registerServerInfoTool, SERVER_INFO_TOOL_NAME, type McpBuildInfo, type RegisterServerInfoToolOptions, type ServedServerInfo, type ServerInfoAnswer, type ServerInfoToolOptions, } from "./server-info.js";
|
|
22
22
|
export { toStrictToolInput, withStrictToolInput, type RenamedFields, type StrictToolInputMessages, type StrictToolInputOptions } from "./strict-input.js";
|
|
23
23
|
export { DEFAULT_TOOL_LIST_RELEASE, describeToolList, fingerprintToolList, listServedTools, measureToolList, type MeasureToolListOptions, type RegisterTools, type ServedTool, type ToolListMeasurement, } from "./tool-list.js";
|
|
24
|
+
export { createToolListSessionId, isToolListSessionCurrent, MCP_SESSION_ID_HEADER, readToolListSessionVersion, } from "./tool-list-session.js";
|
|
24
25
|
export { toolError, toolResult, withToolErrors, type ToolCaller, type ToolCallHook, type ToolCallOutcome, type ToolCallRecord, type ToolErrorHints, type ToolErrorOptions, } from "./tools.js";
|
|
25
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
package/dist/server/index.js
CHANGED
|
@@ -22,5 +22,6 @@ export { createMcpServerFactory } from "./server-factory.js";
|
|
|
22
22
|
export { registerServerInfoTool, SERVER_INFO_TOOL_NAME, } from "./server-info.js";
|
|
23
23
|
export { toStrictToolInput, withStrictToolInput } from "./strict-input.js";
|
|
24
24
|
export { DEFAULT_TOOL_LIST_RELEASE, describeToolList, fingerprintToolList, listServedTools, measureToolList, } from "./tool-list.js";
|
|
25
|
+
export { createToolListSessionId, isToolListSessionCurrent, MCP_SESSION_ID_HEADER, readToolListSessionVersion, } from "./tool-list-session.js";
|
|
25
26
|
export { toolError, toolResult, withToolErrors, } from "./tools.js";
|
|
26
27
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,+FAA+F;AAC/F,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,+FAA+F;AAC/F,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,GAMhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAkD,MAAM,kBAAkB,CAAC;AAC1G,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,gCAAgC,EAChC,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,gBAAgB,GAMjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,uBAAuB,EACvB,4BAA4B,EAC5B,wBAAwB,EACxB,+BAA+B,EAC/B,sBAAsB,GAGvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,EACzB,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GASjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kCAAkC,EAClC,mBAAmB,EACnB,8BAA8B,EAC9B,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,+BAA+B,EAC/B,gCAAgC,EAChC,2BAA2B,EAC3B,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,2BAA2B,EAC3B,YAAY,EACZ,gCAAgC,EAChC,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,GAK7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,iBAAiB,EAA0C,MAAM,eAAe,CAAC;AACrI,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,uBAAuB,EAAyD,MAAM,uBAAuB,CAAC;AAC1L,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,EAAwB,MAAM,cAAc,CAAC;AACtI,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAA+B,MAAM,YAAY,CAAC;AAC9F,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,GAE5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,GAOlB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,eAAe,EACf,cAAc,GAMf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAgC,MAAM,qBAAqB,CAAC;AAC3F,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GAMtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAiF,MAAM,mBAAmB,CAAC;AAC1J,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,eAAe,GAKhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,SAAS,EACT,UAAU,EACV,cAAc,GAOf,MAAM,YAAY,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer, type McpServerOptions } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { McpServerIdentity } from "../contract.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { VersionedMcpServerFactory } from "./endpoint.js";
|
|
4
4
|
import { type StrictToolInputOptions } from "./strict-input.js";
|
|
5
5
|
import { type ServerInfoToolOptions } from "./server-info.js";
|
|
6
6
|
export interface McpServerFactoryOptions {
|
|
@@ -26,6 +26,10 @@ export interface McpServerFactoryOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
readonly serverInfoTool?: ServerInfoToolOptions;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* An `McpServerFactory` whose servers carry the tool list's version for the identity's access level. Its
|
|
31
|
+
* `toolListVersion(identity)` reads that version without building a server, so `createMcpEndpoint` ties each session
|
|
32
|
+
* to it (#1055).
|
|
33
|
+
*/
|
|
34
|
+
export declare function createMcpServerFactory(options: McpServerFactoryOptions): VersionedMcpServerFactory;
|
|
31
35
|
//# sourceMappingURL=server-factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-factory.d.ts","sourceRoot":"","sources":["../../src/server/server-factory.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"server-factory.d.ts","sourceRoot":"","sources":["../../src/server/server-factory.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAuB,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAA0B,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAGtF,MAAM,WAAW,uBAAuB;IACtC,yBAAyB;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,gBAAgB,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,KAAK,OAAO,CAAC;IACpF,yGAAyG;IACzG,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC5D;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACjD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,yBAAyB,CAqClG"}
|
|
@@ -12,7 +12,11 @@ import { McpServer } from "@modelcontextprotocol/server";
|
|
|
12
12
|
import { withStrictToolInput } from "./strict-input.js";
|
|
13
13
|
import { registerServerInfoTool } from "./server-info.js";
|
|
14
14
|
import { measureToolList } from "./tool-list.js";
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* An `McpServerFactory` whose servers carry the tool list's version for the identity's access level. Its
|
|
17
|
+
* `toolListVersion(identity)` reads that version without building a server, so `createMcpEndpoint` ties each session
|
|
18
|
+
* to it (#1055).
|
|
19
|
+
*/
|
|
16
20
|
export function createMcpServerFactory(options) {
|
|
17
21
|
const strictOptions = options.strictToolInput === true ? {} : options.strictToolInput === false ? undefined : options.strictToolInput;
|
|
18
22
|
// The served version reaches `get_server_info` here; the recording server registers it with a placeholder, since
|
|
@@ -39,11 +43,13 @@ export function createMcpServerFactory(options) {
|
|
|
39
43
|
void measurement.catch(() => measurements.delete(identity.canWrite));
|
|
40
44
|
return measurement;
|
|
41
45
|
};
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const toolListVersion = async (identity) => options.version ?? (await measure(identity)).toolListVersion;
|
|
47
|
+
const factory = async (identity) => {
|
|
48
|
+
const version = await toolListVersion(identity);
|
|
44
49
|
const server = new McpServer({ name: options.name, version, ...(options.title === undefined ? {} : { title: options.title }) }, options.serverOptions);
|
|
45
50
|
await registerTools(server, identity, version);
|
|
46
51
|
return server;
|
|
47
52
|
};
|
|
53
|
+
return Object.assign(factory, { toolListVersion });
|
|
48
54
|
}
|
|
49
55
|
//# sourceMappingURL=server-factory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-factory.js","sourceRoot":"","sources":["../../src/server/server-factory.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,kGAAkG;AAClG,mGAAmG;AACnG,qGAAqG;AACrG,6CAA6C;AAC7C,EAAE;AACF,yDAAyD;AACzD,oBAAoB;AACpB,gFAAgF;AAChF,QAAQ;AACR,OAAO,EAAE,SAAS,EAAyB,MAAM,8BAA8B,CAAC;AAGhF,OAAO,EAAE,mBAAmB,EAA+B,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,sBAAsB,EAA8B,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,eAAe,EAA4B,MAAM,gBAAgB,CAAC;AA0B3E
|
|
1
|
+
{"version":3,"file":"server-factory.js","sourceRoot":"","sources":["../../src/server/server-factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The header that carries the session id, both ways. */
|
|
2
|
+
export declare const MCP_SESSION_ID_HEADER = "mcp-session-id";
|
|
3
|
+
/** A new session id for the version: `<base64url(version)>.<32 hex>`, visible ASCII whatever the version holds. */
|
|
4
|
+
export declare function createToolListSessionId(toolListVersion: string): string;
|
|
5
|
+
/** The tool list version a session id carries, or `null` when the id is not one the endpoint issues. */
|
|
6
|
+
export declare function readToolListSessionVersion(sessionId: string): string | null;
|
|
7
|
+
/** Whether a request in this session may be served: its version is the current one. */
|
|
8
|
+
export declare function isToolListSessionCurrent(sessionId: string, toolListVersion: string): boolean;
|
|
9
|
+
/** The answer to a request in a session from another tool list version: the client starts a new session. */
|
|
10
|
+
export declare function createSessionRenewalResponse(): Response;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the request is a JSON-RPC `initialize`: a POST without a session id whose JSON body asks for it. The body
|
|
13
|
+
* is read from a clone, and a body over `maxBytes` is not read (the SDK refuses it with 413 anyway).
|
|
14
|
+
*/
|
|
15
|
+
export declare function isInitializeRequest(request: Request, maxBytes: number): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* The `initialize` answer with `capabilities.tools.listChanged: false`, since no answer of the endpoint ever carries
|
|
18
|
+
* the notification, and with the session id when there is one. The body keeps its format (JSON or one SSE event);
|
|
19
|
+
* an answer that is not a successful `InitializeResult` is returned as it is.
|
|
20
|
+
*/
|
|
21
|
+
export declare function rewriteInitializeAnswer(response: Response, sessionId: string | null): Promise<Response>;
|
|
22
|
+
//# sourceMappingURL=tool-list-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-list-session.d.ts","sourceRoot":"","sources":["../../src/server/tool-list-session.ts"],"names":[],"mappings":"AAQA,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AAOtD,mHAAmH;AACnH,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wGAAwG;AACxG,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI3E;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAE5F;AAED,4GAA4G;AAC5G,wBAAgB,4BAA4B,IAAI,QAAQ,CAKvD;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc9F;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsB7G"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Sessions tied to the tool list version (#1055). A hosted client keeps its cached `tools/list` for as long as its
|
|
2
|
+
// connection lives, even after a release changed a tool, and the endpoint has no channel to send
|
|
3
|
+
// `notifications/tools/list_changed`. So the endpoint answers `initialize` with an `Mcp-Session-Id` that carries the
|
|
4
|
+
// tool list version of the identity's access level, and a later request whose session carries another version gets
|
|
5
|
+
// `404 Not Found`: Streamable HTTP defines that as "start a new session", so the client initializes again and lists
|
|
6
|
+
// the tools again. Nothing is stored: the id itself holds the version, so any instance can check it.
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
/** The header that carries the session id, both ways. */
|
|
9
|
+
export const MCP_SESSION_ID_HEADER = "mcp-session-id";
|
|
10
|
+
/** Random bytes after the version, as hex. */
|
|
11
|
+
const RANDOM_BYTES = 16;
|
|
12
|
+
const SESSION_ID_PATTERN = /^([A-Za-z0-9_-]+)\.[0-9a-f]+$/;
|
|
13
|
+
const SSE_DATA_PREFIX = "data: ";
|
|
14
|
+
/** A new session id for the version: `<base64url(version)>.<32 hex>`, visible ASCII whatever the version holds. */
|
|
15
|
+
export function createToolListSessionId(toolListVersion) {
|
|
16
|
+
return `${Buffer.from(toolListVersion, "utf8").toString("base64url")}.${randomBytes(RANDOM_BYTES).toString("hex")}`;
|
|
17
|
+
}
|
|
18
|
+
/** The tool list version a session id carries, or `null` when the id is not one the endpoint issues. */
|
|
19
|
+
export function readToolListSessionVersion(sessionId) {
|
|
20
|
+
const match = SESSION_ID_PATTERN.exec(sessionId);
|
|
21
|
+
if (match?.[1] === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
return Buffer.from(match[1], "base64url").toString("utf8");
|
|
24
|
+
}
|
|
25
|
+
/** Whether a request in this session may be served: its version is the current one. */
|
|
26
|
+
export function isToolListSessionCurrent(sessionId, toolListVersion) {
|
|
27
|
+
return readToolListSessionVersion(sessionId) === toolListVersion;
|
|
28
|
+
}
|
|
29
|
+
/** The answer to a request in a session from another tool list version: the client starts a new session. */
|
|
30
|
+
export function createSessionRenewalResponse() {
|
|
31
|
+
return Response.json({ jsonrpc: "2.0", error: { code: -32001, message: "Session not found: the tool list changed, initialize again" }, id: null }, { status: 404, headers: { "cache-control": "no-store" } });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the request is a JSON-RPC `initialize`: a POST without a session id whose JSON body asks for it. The body
|
|
35
|
+
* is read from a clone, and a body over `maxBytes` is not read (the SDK refuses it with 413 anyway).
|
|
36
|
+
*/
|
|
37
|
+
export async function isInitializeRequest(request, maxBytes) {
|
|
38
|
+
if (request.method !== "POST" || request.headers.has(MCP_SESSION_ID_HEADER))
|
|
39
|
+
return false;
|
|
40
|
+
const length = Number(request.headers.get("content-length") ?? "0");
|
|
41
|
+
if (!Number.isFinite(length) || length > maxBytes)
|
|
42
|
+
return false;
|
|
43
|
+
let body;
|
|
44
|
+
try {
|
|
45
|
+
const text = await request.clone().text();
|
|
46
|
+
if (text.length > maxBytes)
|
|
47
|
+
return false;
|
|
48
|
+
body = JSON.parse(text);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Not JSON: the SDK answers the parse error itself.
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) && body.method === "initialize";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The `initialize` answer with `capabilities.tools.listChanged: false`, since no answer of the endpoint ever carries
|
|
58
|
+
* the notification, and with the session id when there is one. The body keeps its format (JSON or one SSE event);
|
|
59
|
+
* an answer that is not a successful `InitializeResult` is returned as it is.
|
|
60
|
+
*/
|
|
61
|
+
export async function rewriteInitializeAnswer(response, sessionId) {
|
|
62
|
+
if (response.status !== 200)
|
|
63
|
+
return response;
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
const isEventStream = (response.headers.get("content-type") ?? "").includes("text/event-stream");
|
|
66
|
+
const lines = text.split("\n");
|
|
67
|
+
const dataIndex = isEventStream ? lines.findIndex((line) => line.startsWith(SSE_DATA_PREFIX)) : -1;
|
|
68
|
+
const json = isEventStream ? (dataIndex === -1 ? null : (lines[dataIndex] ?? "").slice(SSE_DATA_PREFIX.length)) : text;
|
|
69
|
+
const answer = parseInitializeAnswer(json);
|
|
70
|
+
const headers = new Headers(response.headers);
|
|
71
|
+
// The body changes length, so the old one would be wrong.
|
|
72
|
+
headers.delete("content-length");
|
|
73
|
+
const tools = answer?.result?.capabilities?.tools;
|
|
74
|
+
if (answer === null || answer.result?.protocolVersion === undefined) {
|
|
75
|
+
return new Response(text, { status: response.status, statusText: response.statusText, headers });
|
|
76
|
+
}
|
|
77
|
+
if (tools !== undefined && tools !== null && typeof tools === "object")
|
|
78
|
+
tools.listChanged = false;
|
|
79
|
+
if (sessionId !== null)
|
|
80
|
+
headers.set(MCP_SESSION_ID_HEADER, sessionId);
|
|
81
|
+
const rewritten = JSON.stringify(answer);
|
|
82
|
+
if (isEventStream)
|
|
83
|
+
lines[dataIndex] = `${SSE_DATA_PREFIX}${rewritten}`;
|
|
84
|
+
return new Response(isEventStream ? lines.join("\n") : rewritten, { status: response.status, statusText: response.statusText, headers });
|
|
85
|
+
}
|
|
86
|
+
/** The answer as JSON, or `null` when there is none or it is not JSON (the SDK's own error text). */
|
|
87
|
+
function parseInitializeAnswer(json) {
|
|
88
|
+
if (json === null)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(json);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=tool-list-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-list-session.js","sourceRoot":"","sources":["../../src/server/tool-list-session.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/testing/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
// `@softure-ai/mcp-access/testing`: checks for an app's own tests.
|
|
2
2
|
export { expectToolCatalogMatchesServer, getToolCatalogDifferences, measureServedToolLists, } from "./catalog.js";
|
|
3
|
+
export { expectToolListSessionRenewed } from "./sessions.js";
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,sBAAsB,GAGvB,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,sBAAsB,GAGvB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpServerFactory } from "../server/endpoint.js";
|
|
2
|
+
/**
|
|
3
|
+
* Throws unless a session opened under `before` (the previous release's factory) is renewed under `after` for every
|
|
4
|
+
* access level whose served tool list changed, and kept for every level whose list did not. A pair where no level
|
|
5
|
+
* changed throws too: the check would prove nothing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function expectToolListSessionRenewed(before: McpServerFactory, after: McpServerFactory): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/testing/sessions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAA6B,MAAM,uBAAuB,CAAC;AAWzF;;;;GAIG;AACH,wBAAsB,4BAA4B,CAAC,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAanH"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createToolListSessionId, isToolListSessionCurrent } from "../server/tool-list-session.js";
|
|
2
|
+
import { fingerprintToolList, listServedTools } from "../server/tool-list.js";
|
|
3
|
+
const READER = { userId: "00000000-0000-4000-8000-000000000001", canWrite: false, tokenId: "session-check" };
|
|
4
|
+
const WRITER = { ...READER, canWrite: true };
|
|
5
|
+
const LEVELS = [
|
|
6
|
+
["read", READER],
|
|
7
|
+
["write", WRITER],
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Throws unless a session opened under `before` (the previous release's factory) is renewed under `after` for every
|
|
11
|
+
* access level whose served tool list changed, and kept for every level whose list did not. A pair where no level
|
|
12
|
+
* changed throws too: the check would prove nothing.
|
|
13
|
+
*/
|
|
14
|
+
export async function expectToolListSessionRenewed(before, after) {
|
|
15
|
+
const problems = [];
|
|
16
|
+
const unchanged = [];
|
|
17
|
+
for (const [level, identity] of LEVELS) {
|
|
18
|
+
const sessionId = createToolListSessionId(await readVersion(before, identity));
|
|
19
|
+
const isCurrent = isToolListSessionCurrent(sessionId, await readVersion(after, identity));
|
|
20
|
+
const hasChanged = fingerprintToolList(await listServedTools(() => before(identity))) !== fingerprintToolList(await listServedTools(() => after(identity)));
|
|
21
|
+
if (!hasChanged)
|
|
22
|
+
unchanged.push(level);
|
|
23
|
+
if (hasChanged && isCurrent)
|
|
24
|
+
problems.push(`the ${level} tool list changed but a session opened before is kept`);
|
|
25
|
+
if (!hasChanged && !isCurrent)
|
|
26
|
+
problems.push(`the ${level} tool list did not change but a session opened before is renewed`);
|
|
27
|
+
}
|
|
28
|
+
if (unchanged.length === LEVELS.length)
|
|
29
|
+
problems.push(`the tool list did not change for any access level (${unchanged.join(", ")})`);
|
|
30
|
+
if (problems.length > 0)
|
|
31
|
+
throw new Error(`@softure-ai/mcp-access: sessions do not follow the tool list: ${problems.join("; ")}`);
|
|
32
|
+
}
|
|
33
|
+
async function readVersion(factory, identity) {
|
|
34
|
+
const reader = factory.toolListVersion;
|
|
35
|
+
if (typeof reader !== "function") {
|
|
36
|
+
throw new Error("@softure-ai/mcp-access: the factory has no toolListVersion; build it with createMcpServerFactory or pass toolListVersion to createMcpEndpoint");
|
|
37
|
+
}
|
|
38
|
+
return reader(identity);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/testing/sessions.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AACnG,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9E,MAAM,MAAM,GAAsB,EAAE,MAAM,EAAE,sCAAsC,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;AAChI,MAAM,MAAM,GAAsB,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAChE,MAAM,MAAM,GAAG;IACb,CAAC,MAAM,EAAE,MAAM,CAAC;IAChB,CAAC,OAAO,EAAE,MAAM,CAAC;CACT,CAAC;AAEX;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,MAAwB,EAAE,KAAuB;IAClG,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,uBAAuB,CAAC,MAAM,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC/E,MAAM,SAAS,GAAG,wBAAwB,CAAC,SAAS,EAAE,MAAM,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1F,MAAM,UAAU,GAAG,mBAAmB,CAAC,MAAM,eAAe,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,mBAAmB,CAAC,MAAM,eAAe,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5J,IAAI,CAAC,UAAU;YAAE,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,UAAU,IAAI,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,KAAK,wDAAwD,CAAC,CAAC;QACjH,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,KAAK,kEAAkE,CAAC,CAAC;IAC/H,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAAE,QAAQ,CAAC,IAAI,CAAC,sDAAsD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrI,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iEAAiE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACnI,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,OAAyB,EAAE,QAA2B;IAC/E,MAAM,MAAM,GAAI,OAA8C,CAAC,eAAe,CAAC;IAC/E,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,+IAA+I,CAAC,CAAC;IACnK,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC"}
|
package/module.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mcp-access",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"dependsOn": { "security": "^0.2.0", "auth": "^0.2.0", "jobs": "^0.1.1?", "ops": "^0.2.0?" },
|
|
5
5
|
"dbSchema": "mcp",
|
|
6
6
|
"tables": ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window", "used_confirmations", "operation_tasks"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/mcp-access",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SOFTURE AI MCP access module: hashed, scoped, expiring access tokens, a rate-limited Bearer endpoint around the app's MCP server factory, and a token page with client setup instructions for Next.js; tool result helpers with a safe error boundary, a stdio entry, a catalog parity check and the softure-mcp prune command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ export const API_TOKEN_RATE_LIMIT_BUCKETS = {
|
|
|
51
51
|
export const mcpAccess = defineModule({
|
|
52
52
|
manifest: {
|
|
53
53
|
id: MODULE_ID,
|
|
54
|
-
version: "0.
|
|
54
|
+
version: "0.5.0",
|
|
55
55
|
dependsOn: { security: "^0.2.0", auth: "^0.2.0", jobs: "^0.1.1?", ops: "^0.2.0?" },
|
|
56
56
|
dbSchema: "mcp",
|
|
57
57
|
tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window", "used_confirmations", "operation_tasks"],
|
package/src/server/endpoint.ts
CHANGED
|
@@ -19,6 +19,14 @@ import { getProtectedResourceMetadataUrl, isOAuthEnabled } from "./oauth-http.js
|
|
|
19
19
|
import { resolveMcpOrigins } from "./origins.js";
|
|
20
20
|
import { MCP_READ_SCOPE, MCP_WRITE_SCOPE } from "./scopes.js";
|
|
21
21
|
import { verifyAccessToken, type McpAccessContext } from "./tokens.js";
|
|
22
|
+
import {
|
|
23
|
+
createSessionRenewalResponse,
|
|
24
|
+
createToolListSessionId,
|
|
25
|
+
isInitializeRequest,
|
|
26
|
+
isToolListSessionCurrent,
|
|
27
|
+
MCP_SESSION_ID_HEADER,
|
|
28
|
+
rewriteInitializeAnswer,
|
|
29
|
+
} from "./tool-list-session.js";
|
|
22
30
|
import { isWritingEnabled } from "./writes-switch.js";
|
|
23
31
|
|
|
24
32
|
export { MCP_READ_SCOPE, MCP_WRITE_SCOPE };
|
|
@@ -37,8 +45,21 @@ const NO_STORE = { "cache-control": "no-store" };
|
|
|
37
45
|
/** The app's MCP server for one request. It registers write tools only when `canWrite` is true. */
|
|
38
46
|
export type McpServerFactory = (identity: McpServerIdentity) => McpServer | Server | Promise<McpServer | Server>;
|
|
39
47
|
|
|
48
|
+
/** Reads the tool list version the factory's servers answer at `initialize` for the identity. */
|
|
49
|
+
export type ToolListVersionReader = (identity: McpServerIdentity) => Promise<string>;
|
|
50
|
+
|
|
51
|
+
/** A factory that can tell its tool list version without building a server; `createMcpServerFactory` returns one. */
|
|
52
|
+
export interface VersionedMcpServerFactory extends McpServerFactory {
|
|
53
|
+
readonly toolListVersion: ToolListVersionReader;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
export interface McpEndpointOptions {
|
|
41
|
-
readonly createServer: McpServerFactory;
|
|
57
|
+
readonly createServer: McpServerFactory | VersionedMcpServerFactory;
|
|
58
|
+
/**
|
|
59
|
+
* The tool list version per identity, for sessions tied to it (#1055). Default: the factory's own
|
|
60
|
+
* `toolListVersion` (`createMcpServerFactory`). Without either, the endpoint issues no session id.
|
|
61
|
+
*/
|
|
62
|
+
readonly toolListVersion?: ToolListVersionReader;
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
/** Serves one request to the MCP endpoint. */
|
|
@@ -50,7 +71,8 @@ export type McpEndpoint = (ctx: McpAccessContext, request: Request) => Promise<R
|
|
|
50
71
|
* (`responseMode: "json"`): a stream would add proxy buffering and timeouts for no tool that needs
|
|
51
72
|
* it, and mid-call notifications are dropped.
|
|
52
73
|
*/
|
|
53
|
-
export function createMcpEndpoint({ createServer }: McpEndpointOptions): McpEndpoint {
|
|
74
|
+
export function createMcpEndpoint({ createServer, toolListVersion }: McpEndpointOptions): McpEndpoint {
|
|
75
|
+
const readToolListVersion = toolListVersion ?? getFactoryToolListVersion(createServer);
|
|
54
76
|
const handler = createMcpHandler(
|
|
55
77
|
(requestContext) => createServer(readIdentity(requestContext.authInfo)),
|
|
56
78
|
{
|
|
@@ -83,8 +105,40 @@ export function createMcpEndpoint({ createServer }: McpEndpointOptions): McpEndp
|
|
|
83
105
|
|
|
84
106
|
const authInfo = await authenticate(ctx, request);
|
|
85
107
|
if (authInfo instanceof Response) return authInfo;
|
|
86
|
-
return
|
|
108
|
+
return serveInSession(request, authInfo);
|
|
87
109
|
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Sessions tied to the tool list version (#1055): a session from another version (a release changed a tool, or the
|
|
113
|
+
* access level changed) answers 404, so the client initializes again and lists the tools again; `initialize` gets a
|
|
114
|
+
* new session id. A request without a session id is served as it always was.
|
|
115
|
+
*/
|
|
116
|
+
async function serveInSession(request: Request, authInfo: AuthInfo): Promise<Response> {
|
|
117
|
+
const sessionId = request.headers.get(MCP_SESSION_ID_HEADER);
|
|
118
|
+
const isInitialize = sessionId === null && (await isInitializeRequest(request, MAX_MCP_REQUEST_BYTES));
|
|
119
|
+
if (readToolListVersion === undefined || (sessionId === null && !isInitialize)) {
|
|
120
|
+
const response = await handler.fetch(request, { authInfo });
|
|
121
|
+
return isInitialize ? rewriteInitializeAnswer(response, null) : response;
|
|
122
|
+
}
|
|
123
|
+
let version;
|
|
124
|
+
try {
|
|
125
|
+
version = await readToolListVersion(readIdentity(authInfo));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Without the version no session can be checked or issued: serve the request statelessly, as before #1055.
|
|
128
|
+
console.error(`@softure-ai/mcp-access: reading the tool list version failed: ${errorLogLabel(error)}`);
|
|
129
|
+
const response = await handler.fetch(request, { authInfo });
|
|
130
|
+
return isInitialize ? rewriteInitializeAnswer(response, null) : response;
|
|
131
|
+
}
|
|
132
|
+
if (sessionId !== null) {
|
|
133
|
+
return isToolListSessionCurrent(sessionId, version) ? handler.fetch(request, { authInfo }) : createSessionRenewalResponse();
|
|
134
|
+
}
|
|
135
|
+
return rewriteInitializeAnswer(await handler.fetch(request, { authInfo }), createToolListSessionId(version));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The factory's own version reader, when it has one (`createMcpServerFactory`). */
|
|
140
|
+
function getFactoryToolListVersion(factory: McpServerFactory | VersionedMcpServerFactory): ToolListVersionReader | undefined {
|
|
141
|
+
return "toolListVersion" in factory && typeof factory.toolListVersion === "function" ? factory.toolListVersion : undefined;
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
/**
|
package/src/server/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ export {
|
|
|
9
9
|
type McpEndpoint,
|
|
10
10
|
type McpEndpointOptions,
|
|
11
11
|
type McpServerFactory,
|
|
12
|
+
type ToolListVersionReader,
|
|
13
|
+
type VersionedMcpServerFactory,
|
|
12
14
|
} from "./endpoint.js";
|
|
13
15
|
export { actionTool, toFormData, type ActionToolOptions, type FormActionHandler } from "./action-tool.js";
|
|
14
16
|
export {
|
|
@@ -155,6 +157,12 @@ export {
|
|
|
155
157
|
type ServedTool,
|
|
156
158
|
type ToolListMeasurement,
|
|
157
159
|
} from "./tool-list.js";
|
|
160
|
+
export {
|
|
161
|
+
createToolListSessionId,
|
|
162
|
+
isToolListSessionCurrent,
|
|
163
|
+
MCP_SESSION_ID_HEADER,
|
|
164
|
+
readToolListSessionVersion,
|
|
165
|
+
} from "./tool-list-session.js";
|
|
158
166
|
export {
|
|
159
167
|
toolError,
|
|
160
168
|
toolResult,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// });
|
|
11
11
|
import { McpServer, type McpServerOptions } from "@modelcontextprotocol/server";
|
|
12
12
|
import type { McpServerIdentity } from "../contract.js";
|
|
13
|
-
import type {
|
|
13
|
+
import type { VersionedMcpServerFactory } from "./endpoint.js";
|
|
14
14
|
import { withStrictToolInput, type StrictToolInputOptions } from "./strict-input.js";
|
|
15
15
|
import { registerServerInfoTool, type ServerInfoToolOptions } from "./server-info.js";
|
|
16
16
|
import { measureToolList, type ToolListMeasurement } from "./tool-list.js";
|
|
@@ -39,8 +39,12 @@ export interface McpServerFactoryOptions {
|
|
|
39
39
|
readonly serverInfoTool?: ServerInfoToolOptions;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* An `McpServerFactory` whose servers carry the tool list's version for the identity's access level. Its
|
|
44
|
+
* `toolListVersion(identity)` reads that version without building a server, so `createMcpEndpoint` ties each session
|
|
45
|
+
* to it (#1055).
|
|
46
|
+
*/
|
|
47
|
+
export function createMcpServerFactory(options: McpServerFactoryOptions): VersionedMcpServerFactory {
|
|
44
48
|
const strictOptions = options.strictToolInput === true ? {} : options.strictToolInput === false ? undefined : options.strictToolInput;
|
|
45
49
|
// The served version reaches `get_server_info` here; the recording server registers it with a placeholder, since
|
|
46
50
|
// its handler never runs there and the version is not part of the listed tool.
|
|
@@ -66,8 +70,9 @@ export function createMcpServerFactory(options: McpServerFactoryOptions): McpSer
|
|
|
66
70
|
return measurement;
|
|
67
71
|
};
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
const toolListVersion = async (identity: McpServerIdentity): Promise<string> => options.version ?? (await measure(identity)).toolListVersion;
|
|
74
|
+
const factory = async (identity: McpServerIdentity): Promise<McpServer> => {
|
|
75
|
+
const version = await toolListVersion(identity);
|
|
71
76
|
const server = new McpServer(
|
|
72
77
|
{ name: options.name, version, ...(options.title === undefined ? {} : { title: options.title }) },
|
|
73
78
|
options.serverOptions,
|
|
@@ -75,4 +80,5 @@ export function createMcpServerFactory(options: McpServerFactoryOptions): McpSer
|
|
|
75
80
|
await registerTools(server, identity, version);
|
|
76
81
|
return server;
|
|
77
82
|
};
|
|
83
|
+
return Object.assign(factory, { toolListVersion });
|
|
78
84
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Sessions tied to the tool list version (#1055). A hosted client keeps its cached `tools/list` for as long as its
|
|
2
|
+
// connection lives, even after a release changed a tool, and the endpoint has no channel to send
|
|
3
|
+
// `notifications/tools/list_changed`. So the endpoint answers `initialize` with an `Mcp-Session-Id` that carries the
|
|
4
|
+
// tool list version of the identity's access level, and a later request whose session carries another version gets
|
|
5
|
+
// `404 Not Found`: Streamable HTTP defines that as "start a new session", so the client initializes again and lists
|
|
6
|
+
// the tools again. Nothing is stored: the id itself holds the version, so any instance can check it.
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
/** The header that carries the session id, both ways. */
|
|
10
|
+
export const MCP_SESSION_ID_HEADER = "mcp-session-id";
|
|
11
|
+
|
|
12
|
+
/** Random bytes after the version, as hex. */
|
|
13
|
+
const RANDOM_BYTES = 16;
|
|
14
|
+
const SESSION_ID_PATTERN = /^([A-Za-z0-9_-]+)\.[0-9a-f]+$/;
|
|
15
|
+
const SSE_DATA_PREFIX = "data: ";
|
|
16
|
+
|
|
17
|
+
/** A new session id for the version: `<base64url(version)>.<32 hex>`, visible ASCII whatever the version holds. */
|
|
18
|
+
export function createToolListSessionId(toolListVersion: string): string {
|
|
19
|
+
return `${Buffer.from(toolListVersion, "utf8").toString("base64url")}.${randomBytes(RANDOM_BYTES).toString("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The tool list version a session id carries, or `null` when the id is not one the endpoint issues. */
|
|
23
|
+
export function readToolListSessionVersion(sessionId: string): string | null {
|
|
24
|
+
const match = SESSION_ID_PATTERN.exec(sessionId);
|
|
25
|
+
if (match?.[1] === undefined) return null;
|
|
26
|
+
return Buffer.from(match[1], "base64url").toString("utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether a request in this session may be served: its version is the current one. */
|
|
30
|
+
export function isToolListSessionCurrent(sessionId: string, toolListVersion: string): boolean {
|
|
31
|
+
return readToolListSessionVersion(sessionId) === toolListVersion;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The answer to a request in a session from another tool list version: the client starts a new session. */
|
|
35
|
+
export function createSessionRenewalResponse(): Response {
|
|
36
|
+
return Response.json(
|
|
37
|
+
{ jsonrpc: "2.0", error: { code: -32001, message: "Session not found: the tool list changed, initialize again" }, id: null },
|
|
38
|
+
{ status: 404, headers: { "cache-control": "no-store" } },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether the request is a JSON-RPC `initialize`: a POST without a session id whose JSON body asks for it. The body
|
|
44
|
+
* is read from a clone, and a body over `maxBytes` is not read (the SDK refuses it with 413 anyway).
|
|
45
|
+
*/
|
|
46
|
+
export async function isInitializeRequest(request: Request, maxBytes: number): Promise<boolean> {
|
|
47
|
+
if (request.method !== "POST" || request.headers.has(MCP_SESSION_ID_HEADER)) return false;
|
|
48
|
+
const length = Number(request.headers.get("content-length") ?? "0");
|
|
49
|
+
if (!Number.isFinite(length) || length > maxBytes) return false;
|
|
50
|
+
let body: unknown;
|
|
51
|
+
try {
|
|
52
|
+
const text = await request.clone().text();
|
|
53
|
+
if (text.length > maxBytes) return false;
|
|
54
|
+
body = JSON.parse(text);
|
|
55
|
+
} catch {
|
|
56
|
+
// Not JSON: the SDK answers the parse error itself.
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) && (body as { method?: unknown }).method === "initialize";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The `initialize` answer with `capabilities.tools.listChanged: false`, since no answer of the endpoint ever carries
|
|
64
|
+
* the notification, and with the session id when there is one. The body keeps its format (JSON or one SSE event);
|
|
65
|
+
* an answer that is not a successful `InitializeResult` is returned as it is.
|
|
66
|
+
*/
|
|
67
|
+
export async function rewriteInitializeAnswer(response: Response, sessionId: string | null): Promise<Response> {
|
|
68
|
+
if (response.status !== 200) return response;
|
|
69
|
+
const text = await response.text();
|
|
70
|
+
const isEventStream = (response.headers.get("content-type") ?? "").includes("text/event-stream");
|
|
71
|
+
const lines = text.split("\n");
|
|
72
|
+
const dataIndex = isEventStream ? lines.findIndex((line) => line.startsWith(SSE_DATA_PREFIX)) : -1;
|
|
73
|
+
const json = isEventStream ? (dataIndex === -1 ? null : (lines[dataIndex] ?? "").slice(SSE_DATA_PREFIX.length)) : text;
|
|
74
|
+
|
|
75
|
+
const answer = parseInitializeAnswer(json);
|
|
76
|
+
const headers = new Headers(response.headers);
|
|
77
|
+
// The body changes length, so the old one would be wrong.
|
|
78
|
+
headers.delete("content-length");
|
|
79
|
+
const tools = answer?.result?.capabilities?.tools;
|
|
80
|
+
if (answer === null || answer.result?.protocolVersion === undefined) {
|
|
81
|
+
return new Response(text, { status: response.status, statusText: response.statusText, headers });
|
|
82
|
+
}
|
|
83
|
+
if (tools !== undefined && tools !== null && typeof tools === "object") tools.listChanged = false;
|
|
84
|
+
if (sessionId !== null) headers.set(MCP_SESSION_ID_HEADER, sessionId);
|
|
85
|
+
|
|
86
|
+
const rewritten = JSON.stringify(answer);
|
|
87
|
+
if (isEventStream) lines[dataIndex] = `${SSE_DATA_PREFIX}${rewritten}`;
|
|
88
|
+
return new Response(isEventStream ? lines.join("\n") : rewritten, { status: response.status, statusText: response.statusText, headers });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The answer as JSON, or `null` when there is none or it is not JSON (the SDK's own error text). */
|
|
92
|
+
function parseInitializeAnswer(json: string | null): InitializeAnswer | null {
|
|
93
|
+
if (json === null) return null;
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(json) as InitializeAnswer;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface InitializeAnswer {
|
|
102
|
+
result?: { protocolVersion?: unknown; capabilities?: { tools?: { listChanged?: boolean } | null } };
|
|
103
|
+
}
|
package/src/testing/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Sessions tied to the tool list version (#1055), checked on the app's factories: a release that changes a tool must
|
|
2
|
+
// renew the sessions opened before it, or a connected client keeps the old schemas. Framework-free: it throws.
|
|
3
|
+
import type { McpServerIdentity } from "../contract.js";
|
|
4
|
+
import type { McpServerFactory, VersionedMcpServerFactory } from "../server/endpoint.js";
|
|
5
|
+
import { createToolListSessionId, isToolListSessionCurrent } from "../server/tool-list-session.js";
|
|
6
|
+
import { fingerprintToolList, listServedTools } from "../server/tool-list.js";
|
|
7
|
+
|
|
8
|
+
const READER: McpServerIdentity = { userId: "00000000-0000-4000-8000-000000000001", canWrite: false, tokenId: "session-check" };
|
|
9
|
+
const WRITER: McpServerIdentity = { ...READER, canWrite: true };
|
|
10
|
+
const LEVELS = [
|
|
11
|
+
["read", READER],
|
|
12
|
+
["write", WRITER],
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Throws unless a session opened under `before` (the previous release's factory) is renewed under `after` for every
|
|
17
|
+
* access level whose served tool list changed, and kept for every level whose list did not. A pair where no level
|
|
18
|
+
* changed throws too: the check would prove nothing.
|
|
19
|
+
*/
|
|
20
|
+
export async function expectToolListSessionRenewed(before: McpServerFactory, after: McpServerFactory): Promise<void> {
|
|
21
|
+
const problems: string[] = [];
|
|
22
|
+
const unchanged: string[] = [];
|
|
23
|
+
for (const [level, identity] of LEVELS) {
|
|
24
|
+
const sessionId = createToolListSessionId(await readVersion(before, identity));
|
|
25
|
+
const isCurrent = isToolListSessionCurrent(sessionId, await readVersion(after, identity));
|
|
26
|
+
const hasChanged = fingerprintToolList(await listServedTools(() => before(identity))) !== fingerprintToolList(await listServedTools(() => after(identity)));
|
|
27
|
+
if (!hasChanged) unchanged.push(level);
|
|
28
|
+
if (hasChanged && isCurrent) problems.push(`the ${level} tool list changed but a session opened before is kept`);
|
|
29
|
+
if (!hasChanged && !isCurrent) problems.push(`the ${level} tool list did not change but a session opened before is renewed`);
|
|
30
|
+
}
|
|
31
|
+
if (unchanged.length === LEVELS.length) problems.push(`the tool list did not change for any access level (${unchanged.join(", ")})`);
|
|
32
|
+
if (problems.length > 0) throw new Error(`@softure-ai/mcp-access: sessions do not follow the tool list: ${problems.join("; ")}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readVersion(factory: McpServerFactory, identity: McpServerIdentity): Promise<string> {
|
|
36
|
+
const reader = (factory as Partial<VersionedMcpServerFactory>).toolListVersion;
|
|
37
|
+
if (typeof reader !== "function") {
|
|
38
|
+
throw new Error("@softure-ai/mcp-access: the factory has no toolListVersion; build it with createMcpServerFactory or pass toolListVersion to createMcpEndpoint");
|
|
39
|
+
}
|
|
40
|
+
return reader(identity);
|
|
41
|
+
}
|