@toolgrain/mcp 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/auth/abort.d.ts +3 -0
- package/dist/auth/abort.d.ts.map +1 -0
- package/dist/auth/abort.js +14 -0
- package/dist/auth/abort.js.map +1 -0
- package/dist/auth/audience.d.ts +3 -0
- package/dist/auth/audience.d.ts.map +1 -0
- package/dist/auth/audience.js +17 -0
- package/dist/auth/audience.js.map +1 -0
- package/dist/auth/authorization.d.ts +7 -0
- package/dist/auth/authorization.d.ts.map +1 -0
- package/dist/auth/authorization.js +58 -0
- package/dist/auth/authorization.js.map +1 -0
- package/dist/auth/context.d.ts +8 -0
- package/dist/auth/context.d.ts.map +1 -0
- package/dist/auth/context.js +37 -0
- package/dist/auth/context.js.map +1 -0
- package/dist/auth/errors.d.ts +14 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +23 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth/types.d.ts +28 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/auth/url.d.ts +3 -0
- package/dist/auth/url.d.ts.map +1 -0
- package/dist/auth/url.js +18 -0
- package/dist/auth/url.js.map +1 -0
- package/dist/connection.d.ts +12 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +151 -0
- package/dist/connection.js.map +1 -0
- package/dist/context/access.d.ts +7 -0
- package/dist/context/access.d.ts.map +1 -0
- package/dist/context/access.js +22 -0
- package/dist/context/access.js.map +1 -0
- package/dist/context/definition.d.ts +24 -0
- package/dist/context/definition.d.ts.map +1 -0
- package/dist/context/definition.js +57 -0
- package/dist/context/definition.js.map +1 -0
- package/dist/context/registry.d.ts +5 -0
- package/dist/context/registry.d.ts.map +1 -0
- package/dist/context/registry.js +15 -0
- package/dist/context/registry.js.map +1 -0
- package/dist/context/scope.d.ts +5 -0
- package/dist/context/scope.d.ts.map +1 -0
- package/dist/context/scope.js +56 -0
- package/dist/context/scope.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +13 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dispatcher.d.ts +19 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +23 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/execute-request.d.ts +7 -0
- package/dist/execute-request.d.ts.map +1 -0
- package/dist/execute-request.js +20 -0
- package/dist/execute-request.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy/connection.d.ts +26 -0
- package/dist/legacy/connection.d.ts.map +1 -0
- package/dist/legacy/connection.js +73 -0
- package/dist/legacy/connection.js.map +1 -0
- package/dist/legacy/dispatcher.d.ts +10 -0
- package/dist/legacy/dispatcher.d.ts.map +1 -0
- package/dist/legacy/dispatcher.js +41 -0
- package/dist/legacy/dispatcher.js.map +1 -0
- package/dist/legacy/handlers.d.ts +12 -0
- package/dist/legacy/handlers.d.ts.map +1 -0
- package/dist/legacy/handlers.js +44 -0
- package/dist/legacy/handlers.js.map +1 -0
- package/dist/legacy/messages.d.ts +31 -0
- package/dist/legacy/messages.d.ts.map +1 -0
- package/dist/legacy/messages.js +27 -0
- package/dist/legacy/messages.js.map +1 -0
- package/dist/legacy/responses.d.ts +5 -0
- package/dist/legacy/responses.d.ts.map +1 -0
- package/dist/legacy/responses.js +8 -0
- package/dist/legacy/responses.js.map +1 -0
- package/dist/legacy/results.d.ts +5 -0
- package/dist/legacy/results.d.ts.map +1 -0
- package/dist/legacy/results.js +19 -0
- package/dist/legacy/results.js.map +1 -0
- package/dist/modern/dispatcher.d.ts +7 -0
- package/dist/modern/dispatcher.d.ts.map +1 -0
- package/dist/modern/dispatcher.js +37 -0
- package/dist/modern/dispatcher.js.map +1 -0
- package/dist/modern/handlers.d.ts +9 -0
- package/dist/modern/handlers.d.ts.map +1 -0
- package/dist/modern/handlers.js +36 -0
- package/dist/modern/handlers.js.map +1 -0
- package/dist/modern/messages.d.ts +142 -0
- package/dist/modern/messages.d.ts.map +1 -0
- package/dist/modern/messages.js +14 -0
- package/dist/modern/messages.js.map +1 -0
- package/dist/modern/metadata.d.ts +45 -0
- package/dist/modern/metadata.d.ts.map +1 -0
- package/dist/modern/metadata.js +49 -0
- package/dist/modern/metadata.js.map +1 -0
- package/dist/modern/responses.d.ts +3 -0
- package/dist/modern/responses.d.ts.map +1 -0
- package/dist/modern/responses.js +15 -0
- package/dist/modern/responses.js.map +1 -0
- package/dist/modern/results.d.ts +4 -0
- package/dist/modern/results.d.ts.map +1 -0
- package/dist/modern/results.js +4 -0
- package/dist/modern/results.js.map +1 -0
- package/dist/pending-requests.d.ts +12 -0
- package/dist/pending-requests.d.ts.map +1 -0
- package/dist/pending-requests.js +30 -0
- package/dist/pending-requests.js.map +1 -0
- package/dist/protocol/decode.d.ts +21 -0
- package/dist/protocol/decode.d.ts.map +1 -0
- package/dist/protocol/decode.js +40 -0
- package/dist/protocol/decode.js.map +1 -0
- package/dist/protocol/icon.d.ts +11 -0
- package/dist/protocol/icon.d.ts.map +1 -0
- package/dist/protocol/icon.js +8 -0
- package/dist/protocol/icon.js.map +1 -0
- package/dist/protocol/messages.d.ts +19 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +21 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol/responses.d.ts +9 -0
- package/dist/protocol/responses.d.ts.map +1 -0
- package/dist/protocol/responses.js +20 -0
- package/dist/protocol/responses.js.map +1 -0
- package/dist/protocol/version-metadata.d.ts +7 -0
- package/dist/protocol/version-metadata.d.ts.map +1 -0
- package/dist/protocol/version-metadata.js +21 -0
- package/dist/protocol/version-metadata.js.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +101 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/adapter.d.ts +12 -0
- package/dist/tools/adapter.d.ts.map +1 -0
- package/dist/tools/adapter.js +45 -0
- package/dist/tools/adapter.js.map +1 -0
- package/dist/tools/call.d.ts +8 -0
- package/dist/tools/call.d.ts.map +1 -0
- package/dist/tools/call.js +9 -0
- package/dist/tools/call.js.map +1 -0
- package/dist/tools/content.d.ts +181 -0
- package/dist/tools/content.d.ts.map +1 -0
- package/dist/tools/content.js +54 -0
- package/dist/tools/content.js.map +1 -0
- package/dist/tools/context.d.ts +36 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +50 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/definition.d.ts +55 -0
- package/dist/tools/definition.d.ts.map +1 -0
- package/dist/tools/definition.js +44 -0
- package/dist/tools/definition.js.map +1 -0
- package/dist/tools/errors.d.ts +11 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +19 -0
- package/dist/tools/errors.js.map +1 -0
- package/dist/tools/json-value.d.ts +15 -0
- package/dist/tools/json-value.d.ts.map +1 -0
- package/dist/tools/json-value.js +42 -0
- package/dist/tools/json-value.js.map +1 -0
- package/dist/tools/name.d.ts +6 -0
- package/dist/tools/name.d.ts.map +1 -0
- package/dist/tools/name.js +13 -0
- package/dist/tools/name.js.map +1 -0
- package/dist/tools/registry.d.ts +14 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +31 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/results.d.ts +14 -0
- package/dist/tools/results.d.ts.map +1 -0
- package/dist/tools/results.js +71 -0
- package/dist/tools/results.js.map +1 -0
- package/dist/tools/rich-result.d.ts +44 -0
- package/dist/tools/rich-result.d.ts.map +1 -0
- package/dist/tools/rich-result.js +24 -0
- package/dist/tools/rich-result.js.map +1 -0
- package/dist/tools/schema.d.ts +18 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/schema.js +70 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/transport-ownership.d.ts +4 -0
- package/dist/transport-ownership.d.ts.map +1 -0
- package/dist/transport-ownership.js +13 -0
- package/dist/transport-ownership.js.map +1 -0
- package/dist/transport.d.ts +24 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +65 -4
- package/src/auth/abort.ts +14 -0
- package/src/auth/audience.ts +22 -0
- package/src/auth/authorization.ts +81 -0
- package/src/auth/context.ts +43 -0
- package/src/auth/errors.ts +35 -0
- package/src/auth/types.ts +34 -0
- package/src/auth/url.ts +22 -0
- package/src/connection.ts +207 -0
- package/src/context/access.ts +36 -0
- package/src/context/definition.ts +100 -0
- package/src/context/registry.ts +25 -0
- package/src/context/scope.ts +82 -0
- package/src/diagnostics.ts +25 -0
- package/src/dispatcher.ts +70 -0
- package/src/execute-request.ts +35 -0
- package/src/index.ts +41 -0
- package/src/legacy/connection.ts +101 -0
- package/src/legacy/dispatcher.ts +72 -0
- package/src/legacy/handlers.ts +76 -0
- package/src/legacy/messages.ts +27 -0
- package/src/legacy/responses.ts +20 -0
- package/src/legacy/results.ts +20 -0
- package/src/modern/dispatcher.ts +77 -0
- package/src/modern/handlers.ts +55 -0
- package/src/modern/messages.ts +14 -0
- package/src/modern/metadata.ts +49 -0
- package/src/modern/responses.ts +25 -0
- package/src/modern/results.ts +6 -0
- package/src/pending-requests.ts +30 -0
- package/src/protocol/decode.ts +69 -0
- package/src/protocol/icon.ts +8 -0
- package/src/protocol/messages.ts +29 -0
- package/src/protocol/responses.ts +27 -0
- package/src/protocol/version-metadata.ts +42 -0
- package/src/server.ts +164 -0
- package/src/tools/adapter.ts +76 -0
- package/src/tools/call.ts +18 -0
- package/src/tools/content.ts +57 -0
- package/src/tools/context.ts +119 -0
- package/src/tools/definition.ts +144 -0
- package/src/tools/errors.ts +21 -0
- package/src/tools/json-value.ts +81 -0
- package/src/tools/name.ts +13 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/results.ts +100 -0
- package/src/tools/rich-result.ts +70 -0
- package/src/tools/schema.ts +86 -0
- package/src/transport-ownership.ts +17 -0
- package/src/transport.ts +34 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type AuthErrorCode =
|
|
2
|
+
| "invalid_request"
|
|
3
|
+
| "invalid_token"
|
|
4
|
+
| "access_denied"
|
|
5
|
+
| "temporarily_unavailable";
|
|
6
|
+
|
|
7
|
+
const brand = Symbol.for("@toolgrain/mcp/AuthError/v1");
|
|
8
|
+
|
|
9
|
+
/** Only an explicitly public description is exposed; provider causes stay private. */
|
|
10
|
+
export class AuthError extends Error {
|
|
11
|
+
readonly [brand] = true;
|
|
12
|
+
readonly description?: string | undefined;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
readonly code: AuthErrorCode,
|
|
16
|
+
options?: ErrorOptions & { description?: string },
|
|
17
|
+
) {
|
|
18
|
+
super(
|
|
19
|
+
options?.description ??
|
|
20
|
+
{
|
|
21
|
+
invalid_request: "Invalid authorization request",
|
|
22
|
+
invalid_token: "Invalid access token",
|
|
23
|
+
access_denied: "Access denied",
|
|
24
|
+
temporarily_unavailable: "Authorization service unavailable",
|
|
25
|
+
}[code],
|
|
26
|
+
options,
|
|
27
|
+
);
|
|
28
|
+
this.name = "AuthError";
|
|
29
|
+
this.description = options?.description;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isAuthError(value: unknown): value is AuthError {
|
|
34
|
+
return value instanceof Error && Reflect.get(value, brand) === true;
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Implemented by integrations; the HTTP adapter supplies the resolved resource. */
|
|
2
|
+
export interface AuthProvider<Identity extends object = object> {
|
|
3
|
+
readonly type: symbol;
|
|
4
|
+
readonly resource?: string;
|
|
5
|
+
readonly authorizationServers: readonly string[];
|
|
6
|
+
verify(accessToken: string, options: AuthVerifyOptions): Promise<Identity>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AuthVerifyOptions {
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
readonly resource: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AuthMetadata {
|
|
15
|
+
readonly resource?: string;
|
|
16
|
+
readonly authorizationServers: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** An opaque, single-use authorization for one server invocation. Always dispose it. */
|
|
20
|
+
export interface AuthScope {
|
|
21
|
+
readonly signal: AbortSignal;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type AuthReadonly<T> = T extends readonly (infer Item)[]
|
|
26
|
+
? readonly AuthReadonly<Item>[]
|
|
27
|
+
: T extends object
|
|
28
|
+
? { readonly [Key in keyof T]: AuthReadonly<T[Key]> }
|
|
29
|
+
: T;
|
|
30
|
+
|
|
31
|
+
export interface AuthIdentity {
|
|
32
|
+
readonly type: symbol;
|
|
33
|
+
readonly value: object;
|
|
34
|
+
}
|
package/src/auth/url.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Canonical external URL; HTTP is permitted only for local development. */
|
|
2
|
+
export function authUrl(value: string): string {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (
|
|
5
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Reject control characters that URL parsing would silently remove.
|
|
6
|
+
/[\u0000-\u0020\u007f]/.test(value) ||
|
|
7
|
+
!/^https?:\/\/[^/]/i.test(value) ||
|
|
8
|
+
(url.protocol !== "https:" &&
|
|
9
|
+
!(
|
|
10
|
+
url.protocol === "http:" &&
|
|
11
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)
|
|
12
|
+
)) ||
|
|
13
|
+
url.username ||
|
|
14
|
+
url.password ||
|
|
15
|
+
value.includes("#") ||
|
|
16
|
+
value.includes("?")
|
|
17
|
+
)
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
"Auth URLs must use HTTPS without credentials, query, or fragment (HTTP loopback is allowed)",
|
|
20
|
+
);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INVALID_REQUEST,
|
|
3
|
+
PARSE_ERROR,
|
|
4
|
+
type ProtocolResponse,
|
|
5
|
+
} from "@toolgrain/protocol";
|
|
6
|
+
import { type ErrorHandler, reportError } from "./diagnostics.js";
|
|
7
|
+
import type { Dispatcher } from "./dispatcher.js";
|
|
8
|
+
import { executeRequest } from "./execute-request.js";
|
|
9
|
+
import { createLegacyConnection } from "./legacy/connection.js";
|
|
10
|
+
import { createPendingRequests } from "./pending-requests.js";
|
|
11
|
+
import { decode } from "./protocol/decode.js";
|
|
12
|
+
import type { Request } from "./protocol/messages.js";
|
|
13
|
+
import { protocolError } from "./protocol/responses.js";
|
|
14
|
+
import type { Server } from "./server.js";
|
|
15
|
+
import type {
|
|
16
|
+
RequestTransport,
|
|
17
|
+
Transport,
|
|
18
|
+
TransportCallbacks,
|
|
19
|
+
} from "./transport.js";
|
|
20
|
+
import { claimTransport } from "./transport-ownership.js";
|
|
21
|
+
|
|
22
|
+
export function createConnection(
|
|
23
|
+
dispatch: Dispatcher,
|
|
24
|
+
onError?: ErrorHandler,
|
|
25
|
+
onClose?: () => void,
|
|
26
|
+
): ConnectionLifecycle {
|
|
27
|
+
return new Connection(dispatch, onError, onClose);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ConnectionLifecycle {
|
|
31
|
+
listen(transport: Transport): Promise<void>;
|
|
32
|
+
listenRequests(transport: RequestTransport, server: Server): Promise<void>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Owns transport attachment, serialized writes, and connection shutdown. */
|
|
37
|
+
class Connection implements ConnectionLifecycle {
|
|
38
|
+
private state: "idle" | "starting" | "listening" | "closed" = "idle";
|
|
39
|
+
private transport: Transport | RequestTransport | undefined;
|
|
40
|
+
private closePromise: Promise<void> | undefined;
|
|
41
|
+
private writes = Promise.resolve();
|
|
42
|
+
private readonly requests = createPendingRequests(() => this.isOpen());
|
|
43
|
+
private readonly legacy = createLegacyConnection();
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly dispatch: Dispatcher,
|
|
47
|
+
private readonly onError?: ErrorHandler,
|
|
48
|
+
private readonly onClose?: () => void,
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
// Public methods remain safe to call detached from the returned server.
|
|
52
|
+
readonly listen = (transport: Transport): Promise<void> =>
|
|
53
|
+
this.attach(transport, () => transport.start(this.transportCallbacks));
|
|
54
|
+
|
|
55
|
+
readonly listenRequests = (
|
|
56
|
+
transport: RequestTransport,
|
|
57
|
+
server: Server,
|
|
58
|
+
): Promise<void> =>
|
|
59
|
+
this.attach(transport, () =>
|
|
60
|
+
transport.start(server, {
|
|
61
|
+
onError: this.transportCallbacks.onError,
|
|
62
|
+
onClose: this.transportCallbacks.onClose,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
private readonly attach = async (
|
|
67
|
+
transport: Transport | RequestTransport,
|
|
68
|
+
start: () => Promise<void>,
|
|
69
|
+
): Promise<void> => {
|
|
70
|
+
if (this.state !== "idle")
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Server can only attach one transport and cannot restart after closing",
|
|
73
|
+
);
|
|
74
|
+
claimTransport(transport);
|
|
75
|
+
this.state = "starting";
|
|
76
|
+
this.transport = transport;
|
|
77
|
+
try {
|
|
78
|
+
await start();
|
|
79
|
+
if (!this.isOpen()) throw new Error("Transport closed during startup");
|
|
80
|
+
this.state = "listening";
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await this.terminate(error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
readonly close = (): Promise<void> => {
|
|
88
|
+
if (this.closePromise) return this.closePromise;
|
|
89
|
+
this.state = "closed";
|
|
90
|
+
this.closePromise = Promise.resolve().then(async () => {
|
|
91
|
+
const transport = this.transport;
|
|
92
|
+
this.transport = undefined;
|
|
93
|
+
await transport?.close();
|
|
94
|
+
});
|
|
95
|
+
// Aborting runs handler listeners synchronously; a nested close reuses this promise.
|
|
96
|
+
this.onClose?.();
|
|
97
|
+
this.requests.clear();
|
|
98
|
+
return this.closePromise;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
private readonly transportCallbacks: TransportCallbacks = {
|
|
102
|
+
onMessage: (message) => {
|
|
103
|
+
void this.receive(message).catch((error) => this.terminate(error));
|
|
104
|
+
},
|
|
105
|
+
onParseError: () => {
|
|
106
|
+
if (this.isOpen())
|
|
107
|
+
void this.send(
|
|
108
|
+
this.legacy.errorResponse(protocolError(PARSE_ERROR, "Parse error")),
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
onError: (error) => {
|
|
112
|
+
void this.terminate(error);
|
|
113
|
+
},
|
|
114
|
+
onClose: () => {
|
|
115
|
+
void this.close().catch((error) => this.report(error));
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
private isOpen(): boolean {
|
|
120
|
+
return this.state === "starting" || this.state === "listening";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private report(error: unknown): void {
|
|
124
|
+
reportError(this.onError, error);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async terminate(error: unknown): Promise<void> {
|
|
128
|
+
this.report(error);
|
|
129
|
+
try {
|
|
130
|
+
await this.close();
|
|
131
|
+
} catch (failure) {
|
|
132
|
+
this.report(failure);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private send(
|
|
137
|
+
message: ProtocolResponse,
|
|
138
|
+
isActive: () => boolean = () => this.isOpen(),
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const write = this.writes.then(async () => {
|
|
141
|
+
if (
|
|
142
|
+
this.isOpen() &&
|
|
143
|
+
isActive() &&
|
|
144
|
+
this.transport &&
|
|
145
|
+
"send" in this.transport
|
|
146
|
+
)
|
|
147
|
+
await this.transport.send(message);
|
|
148
|
+
});
|
|
149
|
+
this.writes = write.catch((error) => this.terminate(error));
|
|
150
|
+
return this.writes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async receive(message: unknown): Promise<void> {
|
|
154
|
+
if (!this.isOpen()) return;
|
|
155
|
+
const incoming = decode(message);
|
|
156
|
+
switch (incoming.kind) {
|
|
157
|
+
case "ignore":
|
|
158
|
+
return;
|
|
159
|
+
case "cancel":
|
|
160
|
+
if (this.legacy.canCancel(incoming.id))
|
|
161
|
+
this.requests.cancel(incoming.id);
|
|
162
|
+
return;
|
|
163
|
+
case "notification":
|
|
164
|
+
this.legacy.notification(incoming.notification);
|
|
165
|
+
return;
|
|
166
|
+
case "error":
|
|
167
|
+
await this.send(this.legacy.errorResponse(incoming.response));
|
|
168
|
+
return;
|
|
169
|
+
case "request":
|
|
170
|
+
await this.handleRequest(incoming.request);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async handleRequest(request: Request): Promise<void> {
|
|
175
|
+
const prepared = this.legacy.prepare(request);
|
|
176
|
+
if (prepared.error) {
|
|
177
|
+
await this.send(prepared.error);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const pending = this.requests.begin(request.id);
|
|
181
|
+
if (!pending) {
|
|
182
|
+
await this.send(
|
|
183
|
+
protocolError(
|
|
184
|
+
INVALID_REQUEST,
|
|
185
|
+
"Request ID is already in use",
|
|
186
|
+
request.id,
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const initialization = this.legacy.begin(request, prepared.protocolVersion);
|
|
192
|
+
try {
|
|
193
|
+
const response = await executeRequest(
|
|
194
|
+
this.dispatch,
|
|
195
|
+
request,
|
|
196
|
+
{ signal: pending.signal, client: this.legacy.client() },
|
|
197
|
+
(error) => this.report(error),
|
|
198
|
+
prepared.protocolVersion,
|
|
199
|
+
);
|
|
200
|
+
initialization?.respond(response);
|
|
201
|
+
if (response) await this.send(response, pending.isActive);
|
|
202
|
+
} finally {
|
|
203
|
+
initialization?.finish();
|
|
204
|
+
pending.finish();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ToolContext } from "../tools/context.js";
|
|
2
|
+
|
|
3
|
+
const key = Symbol.for("@toolgrain/mcp/context-access/v1");
|
|
4
|
+
type Access = (identity: symbol, name: string) => Promise<unknown>;
|
|
5
|
+
|
|
6
|
+
/** Keep own auth properties and shared request data on dependency-aware views. */
|
|
7
|
+
export function withContextAccess(
|
|
8
|
+
ctx: ToolContext,
|
|
9
|
+
access: Access,
|
|
10
|
+
): ToolContext {
|
|
11
|
+
return Object.freeze(
|
|
12
|
+
Object.create(Object.getPrototypeOf(ctx), {
|
|
13
|
+
...Object.getOwnPropertyDescriptors(ctx),
|
|
14
|
+
[key]: { value: access },
|
|
15
|
+
}) as ToolContext,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readContext<Value>(
|
|
20
|
+
ctx: ToolContext,
|
|
21
|
+
identity: symbol,
|
|
22
|
+
name: string,
|
|
23
|
+
): Promise<Value> {
|
|
24
|
+
try {
|
|
25
|
+
const access = Object.getOwnPropertyDescriptor(ctx, key)?.value as
|
|
26
|
+
| Access
|
|
27
|
+
| undefined;
|
|
28
|
+
if (!access)
|
|
29
|
+
throw new Error("Context access requires a Toolgrain-created context");
|
|
30
|
+
ctx.signal.throwIfAborted();
|
|
31
|
+
return access(identity, name) as Promise<Value>;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Accessors always return promises, including invalid contexts and aborts.
|
|
34
|
+
return Promise.reject(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ToolContext } from "../tools/context.js";
|
|
2
|
+
import { readContext } from "./access.js";
|
|
3
|
+
|
|
4
|
+
const brand = Symbol.for("@toolgrain/mcp/context-provider/v1");
|
|
5
|
+
const bindingKey = Symbol.for("@toolgrain/mcp/context-binding/v1");
|
|
6
|
+
|
|
7
|
+
export interface ContextBinding {
|
|
8
|
+
readonly identity: symbol;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly resolve: (ctx: ToolContext) => unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Erase values for heterogeneous registrations without exposing unsafe overrides. */
|
|
14
|
+
export abstract class RegisteredContextProvider {
|
|
15
|
+
abstract readonly name: string;
|
|
16
|
+
abstract readonly use: (ctx: ToolContext) => Promise<unknown>;
|
|
17
|
+
|
|
18
|
+
// Structural branding keeps compatible package copies interoperable.
|
|
19
|
+
"~toolgrain"(): "context@1" {
|
|
20
|
+
return "context@1";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(RegisteredContextProvider.prototype, brand, {
|
|
24
|
+
value: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** An opaque, invariant value contract; use createContext() and provide() for bindings. */
|
|
28
|
+
export abstract class ContextProvider<Value> extends RegisteredContextProvider {
|
|
29
|
+
abstract readonly use: (ctx: ToolContext) => Promise<Value>;
|
|
30
|
+
// A function property prevents widening Value and then overriding it incompatibly.
|
|
31
|
+
abstract readonly provide: (
|
|
32
|
+
resolve: (ctx: ToolContext) => Value | PromiseLike<Value>,
|
|
33
|
+
) => ContextProvider<Value>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class Provider<Value> extends ContextProvider<Value> {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly use: (ctx: ToolContext) => Promise<Value>;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
binding: ContextBinding,
|
|
42
|
+
use = (ctx: ToolContext) =>
|
|
43
|
+
readContext<Value>(ctx, binding.identity, binding.name),
|
|
44
|
+
) {
|
|
45
|
+
super();
|
|
46
|
+
this.name = binding.name;
|
|
47
|
+
this.use = use;
|
|
48
|
+
Object.defineProperty(this, bindingKey, { value: Object.freeze(binding) });
|
|
49
|
+
Object.freeze(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
readonly provide = (
|
|
53
|
+
resolve: (ctx: ToolContext) => Value | PromiseLike<Value>,
|
|
54
|
+
): ContextProvider<Value> => {
|
|
55
|
+
if (typeof resolve !== "function")
|
|
56
|
+
throw new TypeError(
|
|
57
|
+
"Context provider overrides require a resolver function",
|
|
58
|
+
);
|
|
59
|
+
return new Provider<Value>({ ...contextBinding(this), resolve }, this.use);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Infer the resolved value type, including asynchronous factories. */
|
|
64
|
+
export function createContext<Value>(options: {
|
|
65
|
+
name: string;
|
|
66
|
+
resolve: (ctx: ToolContext) => Value;
|
|
67
|
+
}): ContextProvider<Awaited<Value>> {
|
|
68
|
+
if (
|
|
69
|
+
!options ||
|
|
70
|
+
typeof options.name !== "string" ||
|
|
71
|
+
!options.name.trim() ||
|
|
72
|
+
typeof options.resolve !== "function"
|
|
73
|
+
)
|
|
74
|
+
throw new TypeError(
|
|
75
|
+
"Context providers require a nonempty name and a resolver function",
|
|
76
|
+
);
|
|
77
|
+
return new Provider<Awaited<Value>>({
|
|
78
|
+
identity: Symbol(options.name),
|
|
79
|
+
name: options.name,
|
|
80
|
+
resolve: options.resolve,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function contextBinding(provider: unknown): ContextBinding {
|
|
85
|
+
if (
|
|
86
|
+
typeof provider !== "object" ||
|
|
87
|
+
provider === null ||
|
|
88
|
+
(provider as { [brand]?: unknown })[brand] !== true
|
|
89
|
+
)
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
"Context providers must be created with createContext()",
|
|
92
|
+
);
|
|
93
|
+
const binding = Object.getOwnPropertyDescriptor(provider, bindingKey)
|
|
94
|
+
?.value as ContextBinding | undefined;
|
|
95
|
+
if (!binding)
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
"Context providers must be created with createContext()",
|
|
98
|
+
);
|
|
99
|
+
return binding;
|
|
100
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ContextBinding,
|
|
3
|
+
contextBinding,
|
|
4
|
+
type RegisteredContextProvider,
|
|
5
|
+
} from "./definition.js";
|
|
6
|
+
|
|
7
|
+
export type ContextRegistry = ReadonlyMap<symbol, ContextBinding>;
|
|
8
|
+
|
|
9
|
+
/** Snapshot definitions once; provider names are labels, not registration keys. */
|
|
10
|
+
export function createContextRegistry(
|
|
11
|
+
providers: readonly RegisteredContextProvider[] = [],
|
|
12
|
+
): ContextRegistry {
|
|
13
|
+
if (!Array.isArray(providers))
|
|
14
|
+
throw new TypeError("Context registration must be an array of providers");
|
|
15
|
+
const registry = new Map<symbol, ContextBinding>();
|
|
16
|
+
for (const provider of providers) {
|
|
17
|
+
const binding = contextBinding(provider);
|
|
18
|
+
if (registry.has(binding.identity))
|
|
19
|
+
throw new TypeError(
|
|
20
|
+
`Context provider ${JSON.stringify(binding.name)} is registered more than once`,
|
|
21
|
+
);
|
|
22
|
+
registry.set(binding.identity, binding);
|
|
23
|
+
}
|
|
24
|
+
return registry;
|
|
25
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ToolContext } from "../tools/context.js";
|
|
2
|
+
import { withContextAccess } from "./access.js";
|
|
3
|
+
import type { ContextRegistry } from "./registry.js";
|
|
4
|
+
|
|
5
|
+
interface Resolution {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly promise: Promise<unknown>;
|
|
8
|
+
readonly dependencies: Set<Resolution>;
|
|
9
|
+
pending: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** One cache and pending dependency graph per tool context, independent of request IDs. */
|
|
13
|
+
export function createContextScope(
|
|
14
|
+
ctx: ToolContext,
|
|
15
|
+
registry: ContextRegistry,
|
|
16
|
+
): ToolContext {
|
|
17
|
+
const resolutions = new Map<symbol, Resolution>();
|
|
18
|
+
const resolve = (
|
|
19
|
+
identity: symbol,
|
|
20
|
+
name: string,
|
|
21
|
+
caller?: Resolution,
|
|
22
|
+
): Promise<unknown> => {
|
|
23
|
+
const binding = registry.get(identity);
|
|
24
|
+
if (!binding)
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Context provider ${JSON.stringify(name)} is not registered`,
|
|
27
|
+
);
|
|
28
|
+
let resolution = resolutions.get(identity);
|
|
29
|
+
if (!resolution) {
|
|
30
|
+
const current: Resolution = {
|
|
31
|
+
name: binding.name,
|
|
32
|
+
pending: true,
|
|
33
|
+
dependencies: new Set(),
|
|
34
|
+
// Publish the promise before running user code, including synchronous factories.
|
|
35
|
+
promise: Promise.resolve()
|
|
36
|
+
.then(() => {
|
|
37
|
+
ctx.signal.throwIfAborted();
|
|
38
|
+
return binding.resolve(
|
|
39
|
+
withContextAccess(ctx, (key, label) =>
|
|
40
|
+
resolve(key, label, current),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
})
|
|
44
|
+
.then((value) => {
|
|
45
|
+
ctx.signal.throwIfAborted();
|
|
46
|
+
return value;
|
|
47
|
+
})
|
|
48
|
+
.finally(() => {
|
|
49
|
+
current.pending = false;
|
|
50
|
+
current.dependencies.clear();
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
resolutions.set(identity, current);
|
|
54
|
+
resolution = current;
|
|
55
|
+
}
|
|
56
|
+
if (caller?.pending && resolution.pending) {
|
|
57
|
+
const path = dependencyPath(resolution, caller, new Set());
|
|
58
|
+
if (path)
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Context dependency cycle: ${[caller, ...path].map((item) => item.name).join(" -> ")}`,
|
|
61
|
+
);
|
|
62
|
+
caller.dependencies.add(resolution);
|
|
63
|
+
}
|
|
64
|
+
return resolution.promise;
|
|
65
|
+
};
|
|
66
|
+
return withContextAccess(ctx, (identity, name) => resolve(identity, name));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Follow only unfinished resolutions; shared pending dependencies are not cycles. */
|
|
70
|
+
function dependencyPath(
|
|
71
|
+
from: Resolution,
|
|
72
|
+
target: Resolution,
|
|
73
|
+
visited: Set<Resolution>,
|
|
74
|
+
): Resolution[] | undefined {
|
|
75
|
+
if (from === target) return [from];
|
|
76
|
+
if (!from.pending || visited.has(from)) return;
|
|
77
|
+
visited.add(from);
|
|
78
|
+
for (const dependency of from.dependencies) {
|
|
79
|
+
const path = dependencyPath(dependency, target, visited);
|
|
80
|
+
if (path) return [from, ...path];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ToolRequest } from "./tools/context.js";
|
|
2
|
+
|
|
3
|
+
/** Private diagnostics for an unexpected tool failure; no arguments or auth data are added. */
|
|
4
|
+
export interface ToolErrorDetails {
|
|
5
|
+
readonly toolName: string;
|
|
6
|
+
readonly request: ToolRequest;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ErrorHandler = (error: unknown, details?: ToolErrorDetails) => void;
|
|
10
|
+
|
|
11
|
+
/** Diagnostics cannot interrupt a response or cleanup, including rejected async callbacks. */
|
|
12
|
+
export function reportError(
|
|
13
|
+
onError: ErrorHandler | undefined,
|
|
14
|
+
error: unknown,
|
|
15
|
+
details?: ToolErrorDetails,
|
|
16
|
+
): void {
|
|
17
|
+
if (!onError) return;
|
|
18
|
+
try {
|
|
19
|
+
const result =
|
|
20
|
+
details === undefined ? onError(error) : onError(error, details);
|
|
21
|
+
void Promise.resolve(result).catch(() => {});
|
|
22
|
+
} catch {
|
|
23
|
+
/* Diagnostics must not replace the original failure. */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Implementation,
|
|
3
|
+
type JSONRPCErrorResponse,
|
|
4
|
+
LEGACY_PROTOCOL_VERSION,
|
|
5
|
+
type ProtocolResponse,
|
|
6
|
+
type SupportedProtocolVersion,
|
|
7
|
+
} from "@toolgrain/protocol";
|
|
8
|
+
import type { AuthIdentity } from "./auth/types.js";
|
|
9
|
+
import type { ContextRegistry } from "./context/registry.js";
|
|
10
|
+
import {
|
|
11
|
+
createLegacyDispatcher,
|
|
12
|
+
usesLegacyProtocol,
|
|
13
|
+
} from "./legacy/dispatcher.js";
|
|
14
|
+
import { errorResponse as legacyErrorResponse } from "./legacy/responses.js";
|
|
15
|
+
import { createModernDispatcher } from "./modern/dispatcher.js";
|
|
16
|
+
import type { Request } from "./protocol/messages.js";
|
|
17
|
+
import { validateVersionContext } from "./protocol/version-metadata.js";
|
|
18
|
+
import type { ToolAdapter } from "./tools/adapter.js";
|
|
19
|
+
import type { ToolClient } from "./tools/context.js";
|
|
20
|
+
|
|
21
|
+
/** Per-request execution state supplied by the connection or independent caller. */
|
|
22
|
+
export interface RequestScope {
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
/** Client details negotiated by legacy initialization, when the connection has them. */
|
|
25
|
+
client?: ToolClient | undefined;
|
|
26
|
+
auth?: AuthIdentity;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Dispatcher = (
|
|
30
|
+
request: Request,
|
|
31
|
+
scope: RequestScope,
|
|
32
|
+
protocolVersion?: SupportedProtocolVersion,
|
|
33
|
+
) => Promise<ProtocolResponse | undefined>;
|
|
34
|
+
|
|
35
|
+
/** Compose the two protocol adapters around one execution registry. */
|
|
36
|
+
export function createDispatcher(
|
|
37
|
+
tools: ToolAdapter,
|
|
38
|
+
identity: Implementation,
|
|
39
|
+
instructions?: string,
|
|
40
|
+
providers?: ContextRegistry,
|
|
41
|
+
): Dispatcher {
|
|
42
|
+
const legacy = createLegacyDispatcher(
|
|
43
|
+
tools,
|
|
44
|
+
identity,
|
|
45
|
+
instructions,
|
|
46
|
+
providers,
|
|
47
|
+
);
|
|
48
|
+
const modern = createModernDispatcher(
|
|
49
|
+
tools,
|
|
50
|
+
identity,
|
|
51
|
+
instructions,
|
|
52
|
+
providers,
|
|
53
|
+
);
|
|
54
|
+
return (request, scope, protocolVersion) => {
|
|
55
|
+
validateVersionContext(request, protocolVersion);
|
|
56
|
+
return usesLegacyProtocol(request, protocolVersion)
|
|
57
|
+
? legacy(request, scope)
|
|
58
|
+
: modern(request, scope);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Envelope errors have no negotiated connection when processing independent calls. */
|
|
63
|
+
export function formatErrorResponse(
|
|
64
|
+
response: JSONRPCErrorResponse,
|
|
65
|
+
protocolVersion?: SupportedProtocolVersion,
|
|
66
|
+
): ProtocolResponse {
|
|
67
|
+
return protocolVersion === LEGACY_PROTOCOL_VERSION
|
|
68
|
+
? legacyErrorResponse(response)
|
|
69
|
+
: response;
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTERNAL_ERROR,
|
|
3
|
+
type ProtocolResponse,
|
|
4
|
+
type SupportedProtocolVersion,
|
|
5
|
+
} from "@toolgrain/protocol";
|
|
6
|
+
import { type ErrorHandler, reportError } from "./diagnostics.js";
|
|
7
|
+
import type { Dispatcher, RequestScope } from "./dispatcher.js";
|
|
8
|
+
import type { Request } from "./protocol/messages.js";
|
|
9
|
+
import { ProtocolError, protocolError } from "./protocol/responses.js";
|
|
10
|
+
|
|
11
|
+
/** Shared dispatch boundary for connection-scoped and independent requests. */
|
|
12
|
+
export async function executeRequest(
|
|
13
|
+
dispatch: Dispatcher,
|
|
14
|
+
request: Request,
|
|
15
|
+
scope: RequestScope,
|
|
16
|
+
onError?: ErrorHandler,
|
|
17
|
+
protocolVersion?: SupportedProtocolVersion,
|
|
18
|
+
): Promise<ProtocolResponse | undefined> {
|
|
19
|
+
const isActive = () => !scope.signal.aborted;
|
|
20
|
+
try {
|
|
21
|
+
const response = await dispatch(request, scope, protocolVersion);
|
|
22
|
+
return isActive() ? response : undefined;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!isActive()) return;
|
|
25
|
+
if (error instanceof ProtocolError)
|
|
26
|
+
return protocolError(error.code, error.message, request.id, error.data);
|
|
27
|
+
reportError(onError, error);
|
|
28
|
+
return protocolError(
|
|
29
|
+
INTERNAL_ERROR,
|
|
30
|
+
"Internal error",
|
|
31
|
+
request.id,
|
|
32
|
+
undefined,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|