@embassys/ambassador 0.0.0 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/dist/agent-capabilities.d.ts +35 -0
  4. package/dist/agent-capabilities.js +221 -0
  5. package/dist/agent-capabilities.js.map +1 -0
  6. package/dist/ambassador-options.d.ts +9 -0
  7. package/dist/ambassador-options.js +31 -0
  8. package/dist/ambassador-options.js.map +1 -0
  9. package/dist/central-credential.d.ts +41 -0
  10. package/dist/central-credential.js +355 -0
  11. package/dist/central-credential.js.map +1 -0
  12. package/dist/central-enrollment.d.ts +31 -0
  13. package/dist/central-enrollment.js +278 -0
  14. package/dist/central-enrollment.js.map +1 -0
  15. package/dist/central-json.d.ts +8 -0
  16. package/dist/central-json.js +259 -0
  17. package/dist/central-json.js.map +1 -0
  18. package/dist/central-protected-transport.d.ts +21 -0
  19. package/dist/central-protected-transport.js +217 -0
  20. package/dist/central-protected-transport.js.map +1 -0
  21. package/dist/central-rest.d.ts +50 -0
  22. package/dist/central-rest.js +401 -0
  23. package/dist/central-rest.js.map +1 -0
  24. package/dist/cli.d.ts +26 -0
  25. package/dist/cli.js +131 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/credential-store.d.ts +18 -0
  28. package/dist/credential-store.js +517 -0
  29. package/dist/credential-store.js.map +1 -0
  30. package/dist/delivery-profile.d.ts +43 -0
  31. package/dist/delivery-profile.js +253 -0
  32. package/dist/delivery-profile.js.map +1 -0
  33. package/dist/direct-delivery.d.ts +38 -0
  34. package/dist/direct-delivery.js +315 -0
  35. package/dist/direct-delivery.js.map +1 -0
  36. package/dist/dpop.d.ts +28 -0
  37. package/dist/dpop.js +119 -0
  38. package/dist/dpop.js.map +1 -0
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +11 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/gateway-application.d.ts +32 -0
  43. package/dist/gateway-application.js +256 -0
  44. package/dist/gateway-application.js.map +1 -0
  45. package/dist/gateway-paths.d.ts +10 -0
  46. package/dist/gateway-paths.js +28 -0
  47. package/dist/gateway-paths.js.map +1 -0
  48. package/dist/guided-registration.d.ts +23 -0
  49. package/dist/guided-registration.js +117 -0
  50. package/dist/guided-registration.js.map +1 -0
  51. package/dist/identity.d.ts +20 -0
  52. package/dist/identity.js +54 -0
  53. package/dist/identity.js.map +1 -0
  54. package/dist/local-mcp.d.ts +28 -0
  55. package/dist/local-mcp.js +420 -0
  56. package/dist/local-mcp.js.map +1 -0
  57. package/dist/local-tool-result.d.ts +4 -0
  58. package/dist/local-tool-result.js +17 -0
  59. package/dist/local-tool-result.js.map +1 -0
  60. package/dist/mcp-contract.d.ts +10 -0
  61. package/dist/mcp-contract.js +63 -0
  62. package/dist/mcp-contract.js.map +1 -0
  63. package/dist/notification-journal.d.ts +24 -0
  64. package/dist/notification-journal.js +194 -0
  65. package/dist/notification-journal.js.map +1 -0
  66. package/dist/notification-relay.d.ts +31 -0
  67. package/dist/notification-relay.js +203 -0
  68. package/dist/notification-relay.js.map +1 -0
  69. package/dist/process-lock.d.ts +8 -0
  70. package/dist/process-lock.js +122 -0
  71. package/dist/process-lock.js.map +1 -0
  72. package/dist/sqlite-artifact.d.ts +7 -0
  73. package/dist/sqlite-artifact.js +121 -0
  74. package/dist/sqlite-artifact.js.map +1 -0
  75. package/dist/webhook-delivery.d.ts +23 -0
  76. package/dist/webhook-delivery.js +122 -0
  77. package/dist/webhook-delivery.js.map +1 -0
  78. package/docs/getting-started-claude.md +94 -0
  79. package/docs/getting-started-codex.md +103 -0
  80. package/docs/getting-started-gemini.md +87 -0
  81. package/docs/getting-started-hermes.md +84 -0
  82. package/docs/getting-started-openclaw.md +83 -0
  83. package/docs/live-qualification.md +174 -0
  84. package/package.json +48 -7
  85. package/index.js +0 -1
@@ -0,0 +1,117 @@
1
+ import { PRODUCTION_AGENT_CAPABILITIES, resolveAgentCapability, } from "./agent-capabilities.js";
2
+ import { createDeliveryProfile, } from "./delivery-profile.js";
3
+ export class GuidedRegistrationError extends Error {
4
+ code;
5
+ constructor(code) {
6
+ super("Guided registration failed");
7
+ this.code = code;
8
+ this.name = "GuidedRegistrationError";
9
+ }
10
+ }
11
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
12
+ function invalid() {
13
+ return new GuidedRegistrationError("invalid_arguments");
14
+ }
15
+ function isRecord(value) {
16
+ return value !== null && typeof value === "object" && !Array.isArray(value);
17
+ }
18
+ function exactKeys(value, required, optional = []) {
19
+ const allowed = new Set([...required, ...optional]);
20
+ return (required.every((key) => Object.hasOwn(value, key)) &&
21
+ Object.keys(value).every((key) => allowed.has(key)));
22
+ }
23
+ function centralArguments(value) {
24
+ if (typeof value.email !== "string" || value.email.length > 254 || !EMAIL.test(value.email)) {
25
+ throw invalid();
26
+ }
27
+ if (value.display_name !== undefined &&
28
+ (typeof value.display_name !== "string" ||
29
+ value.display_name.length < 1 ||
30
+ value.display_name.length > 128)) {
31
+ throw invalid();
32
+ }
33
+ return {
34
+ email: value.email,
35
+ ...(value.display_name === undefined ? {} : { display_name: value.display_name }),
36
+ };
37
+ }
38
+ function deliveryInput(value) {
39
+ if (!isRecord(value) || typeof value.mode !== "string")
40
+ throw invalid();
41
+ if (value.mode === "direct" && exactKeys(value, ["mode"]))
42
+ return { mode: "direct" };
43
+ if (value.mode === "webhook" &&
44
+ exactKeys(value, ["mode", "url", "secret_env"]) &&
45
+ typeof value.url === "string" &&
46
+ typeof value.secret_env === "string") {
47
+ return { mode: "webhook", url: value.url, secret_env: value.secret_env };
48
+ }
49
+ throw invalid();
50
+ }
51
+ export class GuidedRegistration {
52
+ #registry;
53
+ #profileStore;
54
+ #workingDirectory;
55
+ #environment;
56
+ #registerCentral;
57
+ constructor(options) {
58
+ this.#registry = options.registry ?? PRODUCTION_AGENT_CAPABILITIES;
59
+ this.#profileStore = options.profileStore;
60
+ this.#workingDirectory = options.workingDirectory;
61
+ this.#environment = options.environment;
62
+ this.#registerCentral = options.registerCentral;
63
+ }
64
+ async register(untrustedArguments, clientInfo, signal) {
65
+ const resolution = resolveAgentCapability(clientInfo, this.#registry);
66
+ if (resolution.status === "unsupported") {
67
+ return {
68
+ status: "unsupported_agent",
69
+ message: "This MCP client is not supported by this Ambassador version.",
70
+ };
71
+ }
72
+ if (!isRecord(untrustedArguments) ||
73
+ !exactKeys(untrustedArguments, ["email"], ["display_name", "delivery"])) {
74
+ throw invalid();
75
+ }
76
+ const registration = centralArguments(untrustedArguments);
77
+ const capability = resolution.profile;
78
+ const suppliedDelivery = untrustedArguments.delivery === undefined
79
+ ? undefined
80
+ : deliveryInput(untrustedArguments.delivery);
81
+ let delivery;
82
+ if (suppliedDelivery === undefined) {
83
+ if (capability.modes.length !== 1 || capability.modes[0] !== "direct") {
84
+ return {
85
+ status: "input_required",
86
+ prompt: "How should incoming requests reach this agent?",
87
+ required: ["delivery"],
88
+ default: "direct",
89
+ choices: [
90
+ {
91
+ value: "direct",
92
+ label: `Send directly to this ${capability.displayName} agent`,
93
+ },
94
+ { value: "webhook", label: "Send to a webhook" },
95
+ ],
96
+ };
97
+ }
98
+ delivery = { mode: "direct" };
99
+ }
100
+ else {
101
+ delivery = suppliedDelivery;
102
+ }
103
+ if (!capability.modes.includes(delivery.mode))
104
+ throw invalid();
105
+ try {
106
+ const profile = await createDeliveryProfile(capability, delivery, this.#workingDirectory, this.#environment);
107
+ await this.#profileStore.save(profile);
108
+ return await this.#registerCentral(registration, signal);
109
+ }
110
+ catch (error) {
111
+ if (error instanceof GuidedRegistrationError)
112
+ throw error;
113
+ throw new GuidedRegistrationError("registration_failed");
114
+ }
115
+ }
116
+ }
117
+ //# sourceMappingURL=guided-registration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guided-registration.js","sourceRoot":"","sources":["../src/guided-registration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type CentralCredentialRecord, type LoadedCentralCredential } from "./central-credential.js";
2
+ import type { CredentialStore } from "./credential-store.js";
3
+ export type { CredentialStore } from "./credential-store.js";
4
+ export declare class IdentityError extends Error {
5
+ readonly code: "already_enrolled" | "not_enrolled" | "verification_busy";
6
+ constructor(code: "already_enrolled" | "not_enrolled" | "verification_busy");
7
+ }
8
+ export declare class GatewayIdentity {
9
+ #private;
10
+ private readonly store;
11
+ private readonly nowSeconds;
12
+ private constructor();
13
+ static open(store: CredentialStore, nowSeconds?: () => number): Promise<GatewayIdentity>;
14
+ get enrolled(): boolean;
15
+ credential(): LoadedCentralCredential;
16
+ enroll<T>(operation: () => Promise<{
17
+ readonly credential: CentralCredentialRecord;
18
+ readonly localResult: T;
19
+ }>): Promise<T>;
20
+ }
@@ -0,0 +1,54 @@
1
+ import { parseCentralCredential, serializeCentralCredential, } from "./central-credential.js";
2
+ export class IdentityError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(code);
6
+ this.code = code;
7
+ this.name = "IdentityError";
8
+ }
9
+ }
10
+ export class GatewayIdentity {
11
+ store;
12
+ nowSeconds;
13
+ #credential;
14
+ #commitBusy = false;
15
+ constructor(store, nowSeconds, credential) {
16
+ this.store = store;
17
+ this.nowSeconds = nowSeconds;
18
+ this.#credential = credential;
19
+ }
20
+ static async open(store, nowSeconds = () => Date.now() / 1_000) {
21
+ const stored = await store.load();
22
+ return new GatewayIdentity(store, nowSeconds, stored === undefined ? undefined : parseCentralCredential(stored, nowSeconds));
23
+ }
24
+ get enrolled() {
25
+ return this.#credential !== undefined;
26
+ }
27
+ credential() {
28
+ if (this.#credential === undefined)
29
+ throw new IdentityError("not_enrolled");
30
+ if (this.#credential.token.expiresAt <= Math.floor(this.nowSeconds())) {
31
+ throw new IdentityError("not_enrolled");
32
+ }
33
+ return this.#credential;
34
+ }
35
+ async enroll(operation) {
36
+ if (this.#credential !== undefined)
37
+ throw new IdentityError("already_enrolled");
38
+ if (this.#commitBusy)
39
+ throw new IdentityError("verification_busy");
40
+ this.#commitBusy = true;
41
+ try {
42
+ const result = await operation();
43
+ const serialized = serializeCentralCredential(result.credential);
44
+ const loaded = parseCentralCredential(serialized, this.nowSeconds);
45
+ await this.store.save(serialized);
46
+ this.#credential = loaded;
47
+ return result.localResult;
48
+ }
49
+ finally {
50
+ this.#commitBusy = false;
51
+ }
52
+ }
53
+ }
54
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,yBAAyB,CAAC;AAKjC,MAAM,OAAO,aAAc,SAAQ,KAAK;IACjB,IAAI;IAAzB,YAAqB,IAA+D;QAClF,KAAK,CAAC,IAAI,CAAC,CAAC;oBADO,IAAI;QAEvB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,OAAO,eAAe;IAKP,KAAK;IACL,UAAU;IAL7B,WAAW,CAAsC;IACjD,WAAW,GAAG,KAAK,CAAC;IAEpB,YACmB,KAAsB,EACtB,UAAwB,EACzC,UAAoC;qBAFnB,KAAK;0BACL,UAAU;QAG3B,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;IAChC,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CACf,KAAsB,EACtB,UAAU,GAAiB,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;QAEnD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QAClC,OAAO,IAAI,eAAe,CACxB,KAAK,EACL,UAAU,EACV,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,sBAAsB,CAAC,MAAM,EAAE,UAAU,CAAC,CAC9E,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC;IACxC,CAAC;IAED,UAAU;QACR,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;YAAE,MAAM,IAAI,aAAa,CAAC,cAAc,CAAC,CAAC;QAC5E,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;YACtE,MAAM,IAAI,aAAa,CAAC,cAAc,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,MAAM,CACV,SAGE;QAEF,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;YAAE,MAAM,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAAC;QAChF,IAAI,IAAI,CAAC,WAAW;YAAE,MAAM,IAAI,aAAa,CAAC,mBAAmB,CAAC,CAAC;QACnE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAC;YACjC,MAAM,UAAU,GAAG,0BAA0B,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACjE,MAAM,MAAM,GAAG,sBAAsB,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YACnE,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAClC,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC;YAC1B,OAAO,MAAM,CAAC,WAAW,CAAC;QAC5B,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QAC3B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import type { CentralToolDefinition } from "./mcp-contract.js";
2
+ export interface LocalMcpRouter {
3
+ listTools(): Promise<CentralToolDefinition[]>;
4
+ callTool(name: string, arguments_: Record<string, unknown>, signal: AbortSignal, clientInfo: LocalMcpClientInfo | undefined): Promise<Record<string, unknown>>;
5
+ }
6
+ export interface LocalMcpClientInfo {
7
+ readonly name: string;
8
+ readonly version: string;
9
+ }
10
+ export declare class LocalMcpToolError extends Error {
11
+ readonly code: string;
12
+ readonly retryAfterMs?: number | null | undefined;
13
+ constructor(code: string, retryAfterMs?: number | null | undefined);
14
+ get data(): Record<string, unknown>;
15
+ }
16
+ export interface LocalMcpServerOptions {
17
+ port?: number;
18
+ requestTimeoutMs?: number;
19
+ }
20
+ export declare class LocalMcpServer {
21
+ #private;
22
+ private readonly router;
23
+ constructor(localToken: string, router: LocalMcpRouter, options?: LocalMcpServerOptions);
24
+ get endpoint(): string;
25
+ listen(): Promise<void>;
26
+ sendToolListChanged(): Promise<void>;
27
+ close(): Promise<void>;
28
+ }
@@ -0,0 +1,420 @@
1
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { once } from "node:events";
3
+ import { createServer as createHttpServer, } from "node:http";
4
+ import { toWebRequest } from "@modelcontextprotocol/node";
5
+ import { ProtocolError, Server, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
6
+ import { serializeLocalToolResult } from "./local-tool-result.js";
7
+ const MAX_REQUEST_BYTES = 1024 * 1024;
8
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
9
+ const MAX_HEADERS_BYTES = 16 * 1024;
10
+ const MAX_SESSIONS = 32;
11
+ const MAX_CONCURRENT_TOOL_CALLS = 8;
12
+ const LOCAL_REQUEST_TIMEOUT_MS = 35_000;
13
+ const PROTOCOL_VERSION = "2025-06-18";
14
+ export class LocalMcpToolError extends Error {
15
+ code;
16
+ retryAfterMs;
17
+ constructor(code, retryAfterMs) {
18
+ super("Tool call failed");
19
+ this.code = code;
20
+ this.retryAfterMs = retryAfterMs;
21
+ this.name = "LocalMcpToolError";
22
+ }
23
+ get data() {
24
+ return {
25
+ code: this.code,
26
+ ...(this.retryAfterMs === undefined ? {} : { retry_after_ms: this.retryAfterMs }),
27
+ };
28
+ }
29
+ }
30
+ class RequestBodyTooLarge extends Error {
31
+ }
32
+ class ResponseBodyTooLarge extends Error {
33
+ }
34
+ function safeHttpError(response, status) {
35
+ if (!response.headersSent) {
36
+ response.writeHead(status, {
37
+ "cache-control": "no-store",
38
+ "content-type": "text/plain; charset=utf-8",
39
+ });
40
+ }
41
+ response.end("Request rejected\n");
42
+ }
43
+ function authenticate(value, expectedDigest) {
44
+ const actualDigest = createHash("sha256")
45
+ .update(value ?? "", "utf8")
46
+ .digest();
47
+ return timingSafeEqual(actualDigest, expectedDigest);
48
+ }
49
+ function readJsonBody(request) {
50
+ return new Promise((resolve, reject) => {
51
+ const chunks = [];
52
+ let size = 0;
53
+ let settled = false;
54
+ request.on("data", (chunk) => {
55
+ if (settled)
56
+ return;
57
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
58
+ size += bytes.byteLength;
59
+ if (size > MAX_REQUEST_BYTES) {
60
+ settled = true;
61
+ request.pause();
62
+ reject(new RequestBodyTooLarge());
63
+ return;
64
+ }
65
+ chunks.push(bytes);
66
+ });
67
+ request.once("end", () => {
68
+ if (settled)
69
+ return;
70
+ settled = true;
71
+ try {
72
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
73
+ }
74
+ catch {
75
+ reject(new Error("Invalid request body"));
76
+ }
77
+ });
78
+ request.once("aborted", () => {
79
+ if (settled)
80
+ return;
81
+ settled = true;
82
+ reject(new Error("Request aborted"));
83
+ });
84
+ request.once("error", () => {
85
+ if (settled)
86
+ return;
87
+ settled = true;
88
+ reject(new Error("Request failed"));
89
+ });
90
+ });
91
+ }
92
+ function isObject(value) {
93
+ return value !== null && typeof value === "object" && !Array.isArray(value);
94
+ }
95
+ function isInitializeRequest(value) {
96
+ return isObject(value) && value.method === "initialize";
97
+ }
98
+ function sessionId(request) {
99
+ const value = request.headers["mcp-session-id"];
100
+ return typeof value === "string" && value.length > 0 ? value : undefined;
101
+ }
102
+ function responseHeaders(response) {
103
+ return Object.fromEntries(response.headers.entries());
104
+ }
105
+ async function readBoundedResponse(response) {
106
+ const declared = response.headers.get("content-length");
107
+ if (declared !== null && /^\d+$/u.test(declared) && Number(declared) > MAX_RESPONSE_BYTES) {
108
+ await response.body?.cancel().catch(() => undefined);
109
+ throw new ResponseBodyTooLarge();
110
+ }
111
+ if (response.body === null)
112
+ return new Uint8Array();
113
+ const reader = response.body.getReader();
114
+ const chunks = [];
115
+ let total = 0;
116
+ try {
117
+ while (true) {
118
+ const item = await reader.read();
119
+ if (item.done)
120
+ break;
121
+ total += item.value.byteLength;
122
+ if (total > MAX_RESPONSE_BYTES) {
123
+ await reader.cancel().catch(() => undefined);
124
+ throw new ResponseBodyTooLarge();
125
+ }
126
+ chunks.push(item.value);
127
+ }
128
+ }
129
+ catch (error) {
130
+ if (error instanceof ResponseBodyTooLarge)
131
+ throw error;
132
+ throw new Error("MCP response failed");
133
+ }
134
+ const bytes = new Uint8Array(total);
135
+ let offset = 0;
136
+ for (const chunk of chunks) {
137
+ bytes.set(chunk, offset);
138
+ offset += chunk.byteLength;
139
+ }
140
+ return bytes;
141
+ }
142
+ async function writeWebResponse(request, response, webResponse) {
143
+ if (request.method !== "GET") {
144
+ const bytes = await readBoundedResponse(webResponse);
145
+ response.writeHead(webResponse.status, responseHeaders(webResponse));
146
+ response.end(bytes);
147
+ return;
148
+ }
149
+ response.writeHead(webResponse.status, responseHeaders(webResponse));
150
+ if (webResponse.body === null) {
151
+ response.end();
152
+ return;
153
+ }
154
+ const reader = webResponse.body.getReader();
155
+ let total = 0;
156
+ const cancel = () => {
157
+ void reader.cancel().catch(() => undefined);
158
+ };
159
+ response.once("close", cancel);
160
+ try {
161
+ while (!response.destroyed) {
162
+ const item = await reader.read();
163
+ if (item.done)
164
+ break;
165
+ total += item.value.byteLength;
166
+ if (total > MAX_RESPONSE_BYTES) {
167
+ await reader.cancel().catch(() => undefined);
168
+ break;
169
+ }
170
+ if (!response.write(item.value))
171
+ await once(response, "drain");
172
+ }
173
+ }
174
+ finally {
175
+ response.off("close", cancel);
176
+ if (!response.destroyed)
177
+ response.end();
178
+ }
179
+ }
180
+ export class LocalMcpServer {
181
+ router;
182
+ #http;
183
+ #expectedAuthorizationDigest;
184
+ #requestTimeoutMs;
185
+ #port;
186
+ #sessions = new Map();
187
+ #sessionRecords = new Set();
188
+ #activeToolCalls = 0;
189
+ #accepting = false;
190
+ #endpoint;
191
+ constructor(localToken, router, options = {}) {
192
+ this.router = router;
193
+ this.#port = options.port ?? 8787;
194
+ this.#requestTimeoutMs = options.requestTimeoutMs ?? LOCAL_REQUEST_TIMEOUT_MS;
195
+ if (!Number.isInteger(this.#port) || this.#port < 0 || this.#port > 65_535) {
196
+ throw new Error("Invalid MCP listener port");
197
+ }
198
+ if (!Number.isFinite(this.#requestTimeoutMs) || this.#requestTimeoutMs <= 0) {
199
+ throw new Error("Invalid MCP request timeout");
200
+ }
201
+ this.#expectedAuthorizationDigest = createHash("sha256")
202
+ .update(`Bearer ${localToken}`, "utf8")
203
+ .digest();
204
+ this.#http = createHttpServer({ maxHeaderSize: MAX_HEADERS_BYTES, requestTimeout: this.#requestTimeoutMs }, (request, response) => {
205
+ void this.#handleRequest(request, response);
206
+ });
207
+ }
208
+ get endpoint() {
209
+ if (this.#endpoint === undefined)
210
+ throw new Error("MCP listener is not bound");
211
+ return this.#endpoint;
212
+ }
213
+ async listen() {
214
+ if (this.#accepting || this.#endpoint !== undefined) {
215
+ throw new Error("MCP listener is already bound");
216
+ }
217
+ await new Promise((resolve, reject) => {
218
+ const onError = () => reject(new Error("MCP listener failed to bind"));
219
+ this.#http.once("error", onError);
220
+ this.#http.listen(this.#port, "127.0.0.1", () => {
221
+ this.#http.off("error", onError);
222
+ resolve();
223
+ });
224
+ });
225
+ const address = this.#http.address();
226
+ this.#endpoint = `http://127.0.0.1:${address.port}/mcp`;
227
+ this.#accepting = true;
228
+ }
229
+ async sendToolListChanged() {
230
+ await Promise.all([...this.#sessionRecords].map((record) => record.sdk.sendToolListChanged()));
231
+ }
232
+ async close() {
233
+ this.#accepting = false;
234
+ const closed = new Promise((resolve) => {
235
+ if (!this.#http.listening) {
236
+ resolve();
237
+ return;
238
+ }
239
+ this.#http.close(() => resolve());
240
+ });
241
+ await Promise.all([...this.#sessionRecords].map((record) => this.#closeSession(record)));
242
+ this.#http.closeAllConnections();
243
+ await closed;
244
+ }
245
+ #createSdk() {
246
+ const sdk = new Server({ name: "ambassador", version: "1" }, {
247
+ capabilities: { tools: { listChanged: true } },
248
+ supportedProtocolVersions: [PROTOCOL_VERSION],
249
+ debouncedNotificationMethods: ["notifications/tools/list_changed"],
250
+ });
251
+ sdk.onerror = () => undefined;
252
+ sdk.setRequestHandler("tools/list", async () => {
253
+ const tools = await this.router.listTools();
254
+ return { tools: tools };
255
+ });
256
+ sdk.setRequestHandler("tools/call", async (request, context) => {
257
+ if (this.#activeToolCalls >= MAX_CONCURRENT_TOOL_CALLS) {
258
+ throw new Error("Tool call capacity reached");
259
+ }
260
+ const arguments_ = request.params.arguments;
261
+ if (arguments_ !== undefined && !isObject(arguments_)) {
262
+ throw new Error("Invalid tool arguments");
263
+ }
264
+ this.#activeToolCalls += 1;
265
+ try {
266
+ const signal = AbortSignal.any([
267
+ context.mcpReq.signal,
268
+ AbortSignal.timeout(this.#requestTimeoutMs),
269
+ ]);
270
+ const version = sdk.getClientVersion();
271
+ const clientInfo = version !== undefined &&
272
+ typeof version.name === "string" &&
273
+ typeof version.version === "string"
274
+ ? { name: version.name, version: version.version }
275
+ : undefined;
276
+ const result = await this.router.callTool(request.params.name, arguments_ ?? {}, signal, clientInfo);
277
+ const serialized = serializeLocalToolResult(result);
278
+ return sdk.projectCallToolResult({
279
+ content: [{ type: "text", text: serialized }],
280
+ structuredContent: result,
281
+ }, undefined);
282
+ }
283
+ catch (error) {
284
+ if (error instanceof LocalMcpToolError) {
285
+ throw new ProtocolError(-32_002, "Tool call failed", error.data);
286
+ }
287
+ throw new Error("Tool call failed");
288
+ }
289
+ finally {
290
+ this.#activeToolCalls -= 1;
291
+ }
292
+ });
293
+ return sdk;
294
+ }
295
+ async #createSession() {
296
+ if (this.#sessionRecords.size >= MAX_SESSIONS) {
297
+ throw new Error("MCP session capacity reached");
298
+ }
299
+ const sdk = this.#createSdk();
300
+ let record;
301
+ const transport = new WebStandardStreamableHTTPServerTransport({
302
+ sessionIdGenerator: randomUUID,
303
+ keepAliveMs: 15_000,
304
+ onsessioninitialized: (id) => {
305
+ if (this.#sessions.has(id))
306
+ throw new Error("Duplicate MCP session");
307
+ record.id = id;
308
+ this.#sessions.set(id, record);
309
+ },
310
+ onsessionclosed: (id) => {
311
+ this.#sessions.delete(id);
312
+ void this.#closeSession(record);
313
+ },
314
+ });
315
+ record = { sdk, transport, closing: false };
316
+ this.#sessionRecords.add(record);
317
+ try {
318
+ await sdk.connect(transport);
319
+ return record;
320
+ }
321
+ catch (error) {
322
+ this.#sessionRecords.delete(record);
323
+ await sdk.close().catch(() => undefined);
324
+ throw error;
325
+ }
326
+ }
327
+ async #closeSession(record) {
328
+ if (record.closing)
329
+ return;
330
+ record.closing = true;
331
+ if (record.id !== undefined)
332
+ this.#sessions.delete(record.id);
333
+ this.#sessionRecords.delete(record);
334
+ await record.sdk.close().catch(() => undefined);
335
+ }
336
+ async #handleRequest(request, response) {
337
+ if (!this.#accepting) {
338
+ safeHttpError(response, 503);
339
+ return;
340
+ }
341
+ if (request.url !== "/mcp") {
342
+ safeHttpError(response, 404);
343
+ return;
344
+ }
345
+ const address = this.#http.address();
346
+ if (request.headers.host !== `127.0.0.1:${address.port}`) {
347
+ safeHttpError(response, 421);
348
+ return;
349
+ }
350
+ const origin = request.headers.origin;
351
+ if (origin !== undefined && origin !== `http://127.0.0.1:${address.port}`) {
352
+ safeHttpError(response, 403);
353
+ return;
354
+ }
355
+ if (!authenticate(request.headers.authorization, this.#expectedAuthorizationDigest)) {
356
+ safeHttpError(response, 401);
357
+ return;
358
+ }
359
+ const controller = new AbortController();
360
+ const timeout = setTimeout(() => controller.abort(), this.#requestTimeoutMs);
361
+ const onResponseClose = () => {
362
+ if (!response.writableEnded)
363
+ controller.abort();
364
+ };
365
+ response.once("close", onResponseClose);
366
+ let record;
367
+ let transientSession = false;
368
+ try {
369
+ const parsedBody = request.method === "POST" ? await readJsonBody(request) : undefined;
370
+ if (Array.isArray(parsedBody)) {
371
+ safeHttpError(response, 400);
372
+ return;
373
+ }
374
+ const id = sessionId(request);
375
+ if (id === undefined) {
376
+ if (request.method !== "POST" || !isInitializeRequest(parsedBody)) {
377
+ safeHttpError(response, 400);
378
+ return;
379
+ }
380
+ try {
381
+ record = await this.#createSession();
382
+ transientSession = true;
383
+ }
384
+ catch {
385
+ safeHttpError(response, 503);
386
+ return;
387
+ }
388
+ }
389
+ else {
390
+ record = this.#sessions.get(id);
391
+ if (record === undefined) {
392
+ safeHttpError(response, 404);
393
+ return;
394
+ }
395
+ }
396
+ const webRequest = await toWebRequest(request, parsedBody, { signal: controller.signal });
397
+ const webResponse = await record.transport.handleRequest(webRequest, { parsedBody });
398
+ await writeWebResponse(request, response, webResponse);
399
+ if (transientSession && record.id === undefined)
400
+ await this.#closeSession(record);
401
+ }
402
+ catch (error) {
403
+ if (transientSession && record !== undefined && record.id === undefined) {
404
+ await this.#closeSession(record);
405
+ }
406
+ if (error instanceof RequestBodyTooLarge) {
407
+ request.resume();
408
+ safeHttpError(response, 413);
409
+ return;
410
+ }
411
+ if (!response.headersSent)
412
+ safeHttpError(response, 400);
413
+ }
414
+ finally {
415
+ clearTimeout(timeout);
416
+ response.off("close", onResponseClose);
417
+ }
418
+ }
419
+ }
420
+ //# sourceMappingURL=local-mcp.js.map