@agentwares/agentguard 0.1.5 → 0.1.7

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/dist/index.js CHANGED
@@ -840,1826 +840,2172 @@ import {
840
840
  classifyTool,
841
841
  upstreamHeaders
842
842
  } from "@agentwares/agentguard-core";
843
- var ProxyRuntime = class {
844
- loaded;
845
- guard;
846
- agent;
847
- defaultRunId;
848
- sessions = /* @__PURE__ */ new Set();
849
- connections = /* @__PURE__ */ new Map();
850
- exposed = [];
851
- resourceOwner = /* @__PURE__ */ new Map();
852
- templateOwner = /* @__PURE__ */ new Map();
853
- promptOwner = /* @__PURE__ */ new Map();
854
- log;
855
- connectTimeoutMs;
856
- testClients;
857
- /** stop relaying upstream stderr once we are tearing down — shutdown noise is not the user's problem */
858
- closing = false;
859
- constructor(opts) {
860
- this.loaded = opts.loaded;
861
- this.guard = opts.guard;
862
- this.agent = opts.agent;
863
- this.defaultRunId = opts.defaultRunId;
864
- this.log = opts.log ?? ((line) => process.stderr.write(`[agentguard] ${line}
865
- `));
866
- this.connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
867
- this.testClients = opts.clients ?? {};
843
+
844
+ // ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
845
+ import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
846
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
847
+ import {
848
+ CreateMessageResultSchema,
849
+ ElicitResultSchema,
850
+ SubscribeRequestSchema,
851
+ UnsubscribeRequestSchema
852
+ } from "@modelcontextprotocol/sdk/types.js";
853
+ import { z } from "zod";
854
+
855
+ // ../../packages/x402/dist/index.js
856
+ var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
857
+ var USDC_DECIMALS = 6;
858
+ var USDC_EIP712_DOMAIN = { name: "USD Coin", version: "2" };
859
+ function toAtomicUnits(amountUsd, decimals = USDC_DECIMALS) {
860
+ const trimmed = amountUsd.trim();
861
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) throw new Error(`not a decimal amount: ${amountUsd}`);
862
+ const [whole = "0", fraction = ""] = trimmed.split(".");
863
+ if (fraction.length > decimals) {
864
+ throw new Error(`${amountUsd} has more than ${decimals} decimal places`);
865
+ }
866
+ return `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
867
+ }
868
+ var DEFAULT_X402_NETWORK = "eip155:8453";
869
+ var X402_HEADER = "X-Payment-Requirements";
870
+ var X402_REQUIRED_HEADER = "PAYMENT-REQUIRED";
871
+ function toAmount(priceUsd) {
872
+ if (!Number.isFinite(priceUsd) || priceUsd < 0)
873
+ throw new Error("priceUsd must be a non-negative number");
874
+ return priceUsd.toFixed(6).replace(/0+$/, "").replace(/\.$/, ".0");
875
+ }
876
+ function buildPaymentRequired(opts) {
877
+ const amountUsd = toAmount(opts.priceUsd);
878
+ const creditsPayTo = opts.creditsPayTo ?? opts.selfServe?.keysUrl;
879
+ const accepts = [];
880
+ if (opts.payTo) {
881
+ accepts.push({
882
+ rail: "x402",
883
+ scheme: "exact",
884
+ amountUsd,
885
+ amount: toAtomicUnits(amountUsd, opts.assetDecimals),
886
+ resource: opts.resource,
887
+ description: opts.description,
888
+ mimeType: "application/json",
889
+ network: opts.network ?? DEFAULT_X402_NETWORK,
890
+ asset: opts.asset ?? USDC_BASE,
891
+ extra: opts.assetDomain ?? USDC_EIP712_DOMAIN,
892
+ payTo: opts.payTo,
893
+ maxTimeoutSeconds: 300,
894
+ ...opts.bazaar ? { bazaar: opts.bazaar } : {}
895
+ });
896
+ }
897
+ if (creditsPayTo) {
898
+ accepts.push({
899
+ rail: "prepaid_credits",
900
+ amountUsd,
901
+ resource: opts.resource,
902
+ description: opts.description,
903
+ currency: "usd",
904
+ payTo: creditsPayTo,
905
+ maxTimeoutSeconds: 300,
906
+ // funding a balance ends at a hosted Stripe Checkout page, which an agent cannot fill in
907
+ humanRequired: true
908
+ });
909
+ }
910
+ const live = opts.live === true;
911
+ const selfServe = opts.selfServe;
912
+ const x402Rail = accepts.find((a) => a.rail === "x402" && a.payTo);
913
+ const usdcFix = x402Rail ? `Pay in USDC with no account and no human: take the \`accepts\` entry with \`rail: "x402"\`, sign an EIP-3009 transferWithAuthorization for ${x402Rail.amountUsd} USDC to ${x402Rail.payTo} on ${x402Rail.network}, and retry with it base64 in the X-PAYMENT header. The facilitator submits the transfer, so you need USDC and no gas. ` : "";
914
+ const sampleFix = opts.sampleMode ? "Free first: retry with sample=true \u2014 no key, no payment. " : "";
915
+ const cardFix = selfServe ? `Or pay by card: POST ${selfServe.keysUrl} (no auth, no signup) returns a key and a Stripe Checkout URL. Funding that key needs a person at a browser; if you are running unattended, surface the URL to your operator rather than retrying, or use the USDC rail above. Once funded, send Authorization: Bearer <key>, plus Idempotency-Key so a repeated call is charged once.` : "";
916
+ const liveFix = usdcFix || cardFix ? `${sampleFix}${usdcFix}${cardFix}`.trim() : "Retry with a PAYMENT header satisfying one of `accepts`, or send an API key with credit balance.";
917
+ const body = {
918
+ code: "PAYMENT_REQUIRED",
919
+ cause: live ? `This call costs $${amountUsd}. No valid payment or credit was presented.` : `This call costs $${amountUsd}. Pay-per-call (x402 / Stripe MPP) is coming soon; use an API key with prepaid credits.`,
920
+ fix: live ? liveFix : `Create an API key${opts.signupUrl ? ` at ${opts.signupUrl}` : ""} and retry with Authorization: Bearer <key>. Try the free sample mode first.`,
921
+ retryable: true,
922
+ priceUsd: opts.priceUsd,
923
+ accepts,
924
+ comingSoon: !live,
925
+ alternatives: [
926
+ ...creditsPayTo ? ["api_key_credits"] : [],
927
+ ...opts.sampleMode ? ["sample_mode"] : []
928
+ ],
929
+ ...selfServe ? { selfServe } : {},
930
+ ...opts.bazaar ? { extensions: { bazaar: opts.bazaar } } : {}
931
+ };
932
+ const encoded = JSON.stringify({
933
+ x402Version: 2,
934
+ accepts,
935
+ resource: opts.resource,
936
+ ...opts.bazaar ? { extensions: { bazaar: opts.bazaar } } : {}
937
+ });
938
+ const headers = {
939
+ "Content-Type": "application/json",
940
+ "WWW-Authenticate": `Payment realm="agentwares", methods="x402 prepaid_credits", price="${amountUsd} USD"`,
941
+ [X402_HEADER]: encoded,
942
+ [X402_REQUIRED_HEADER]: btoa(encoded),
943
+ "Cache-Control": "no-store"
944
+ };
945
+ return { status: 402, headers, body };
946
+ }
947
+ var StubVerifier = class {
948
+ async verify(request, requirement) {
949
+ const hasHeader = request.headers.has("PAYMENT") || request.headers.has("X-PAYMENT");
950
+ return {
951
+ ok: false,
952
+ rail: requirement.rail,
953
+ reason: hasHeader ? "payments are not live yet (phase 2: wallet). Use an API key with credits." : "no payment header present"
954
+ };
955
+ }
956
+ };
957
+
958
+ // ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
959
+ import { z as z2 } from "zod";
960
+ import { z as z3 } from "zod";
961
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
962
+ import {
963
+ CallToolRequestSchema,
964
+ ErrorCode,
965
+ ListToolsRequestSchema,
966
+ McpError
967
+ } from "@modelcontextprotocol/sdk/types.js";
968
+ import { z as z4 } from "zod";
969
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
970
+ var ERROR_CODES = [
971
+ "INVALID_INPUT",
972
+ "NOT_FOUND",
973
+ "UNAUTHORIZED",
974
+ "PAYMENT_REQUIRED",
975
+ "RATE_LIMITED",
976
+ "UPSTREAM_ERROR",
977
+ "INTERNAL"
978
+ ];
979
+ var DEFAULT_RETRYABLE = {
980
+ INVALID_INPUT: false,
981
+ NOT_FOUND: false,
982
+ UNAUTHORIZED: false,
983
+ PAYMENT_REQUIRED: true,
984
+ RATE_LIMITED: true,
985
+ UPSTREAM_ERROR: true,
986
+ INTERNAL: false
987
+ };
988
+ var DEFAULT_HTTP_STATUS = {
989
+ INVALID_INPUT: 400,
990
+ NOT_FOUND: 404,
991
+ UNAUTHORIZED: 401,
992
+ PAYMENT_REQUIRED: 402,
993
+ RATE_LIMITED: 429,
994
+ UPSTREAM_ERROR: 502,
995
+ INTERNAL: 500
996
+ };
997
+ var INTERNAL_FIX = "retry; if it persists, report the request id";
998
+ function isStandardCode(code) {
999
+ return ERROR_CODES.includes(code);
1000
+ }
1001
+ var McpToolError = class extends Error {
1002
+ code;
1003
+ cause;
1004
+ fix;
1005
+ retryable;
1006
+ details;
1007
+ httpStatus;
1008
+ extra;
1009
+ constructor(init) {
1010
+ super(`${init.code}: ${init.cause}`);
1011
+ this.name = "McpToolError";
1012
+ this.code = init.code;
1013
+ this.cause = init.cause;
1014
+ this.fix = init.fix;
1015
+ this.retryable = init.retryable ?? (isStandardCode(init.code) ? DEFAULT_RETRYABLE[init.code] : false);
1016
+ this.details = init.details;
1017
+ this.httpStatus = init.httpStatus ?? (isStandardCode(init.code) ? DEFAULT_HTTP_STATUS[init.code] : 500);
1018
+ this.extra = init.extra;
1019
+ }
1020
+ /** The body agents see: `{ code, cause, fix, retryable, details?, ...extra }`. */
1021
+ toJSON() {
1022
+ const body = {
1023
+ ...this.extra,
1024
+ code: this.code,
1025
+ cause: this.cause,
1026
+ fix: this.fix,
1027
+ retryable: this.retryable
1028
+ };
1029
+ if (this.details !== void 0) body.details = this.details;
1030
+ return body;
1031
+ }
1032
+ };
1033
+ function toolError(codeOrInit, cause, fix, extra) {
1034
+ if (typeof codeOrInit === "object") return new McpToolError(codeOrInit);
1035
+ return new McpToolError({ code: codeOrInit, cause: cause ?? "", fix: fix ?? "", ...extra });
1036
+ }
1037
+ function describeUnknown(err) {
1038
+ if (err instanceof Error) return err.message || err.name;
1039
+ if (typeof err === "string") return err;
1040
+ try {
1041
+ return JSON.stringify(err);
1042
+ } catch {
1043
+ return String(err);
1044
+ }
1045
+ }
1046
+ function normalizeError(err) {
1047
+ if (err instanceof McpToolError) return err;
1048
+ return new McpToolError({
1049
+ code: "INTERNAL",
1050
+ cause: describeUnknown(err),
1051
+ fix: INTERNAL_FIX,
1052
+ retryable: false
1053
+ });
1054
+ }
1055
+ function errorResult(err, opts = {}) {
1056
+ const error = normalizeError(err);
1057
+ const body = error.toJSON();
1058
+ const meta = { httpStatus: error.httpStatus };
1059
+ if (opts.requestId !== void 0) meta.requestId = opts.requestId;
1060
+ Object.assign(meta, opts.meta);
1061
+ return {
1062
+ isError: true,
1063
+ content: [{ type: "text", text: JSON.stringify(body) }],
1064
+ structuredContent: body,
1065
+ _meta: meta
1066
+ };
1067
+ }
1068
+ function createToolContext(init = {}) {
1069
+ const { headers, ...rest } = init;
1070
+ return { headers: headers ?? new Headers(), ...rest };
1071
+ }
1072
+ var TOOL_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
1073
+ var MIN_NAMESPACE_LENGTH = 3;
1074
+ var MIN_DESCRIPTION_LENGTH = 20;
1075
+ function isValidToolName(name) {
1076
+ if (!TOOL_NAME_PATTERN.test(name)) return false;
1077
+ const namespace = name.split("_")[0] ?? "";
1078
+ return namespace.length >= MIN_NAMESPACE_LENGTH;
1079
+ }
1080
+ function assertToolName(name) {
1081
+ if (!isValidToolName(name)) {
1082
+ throw new Error(
1083
+ `tool name must be namespaced: <namespace>_<verb>_<object>, got ${JSON.stringify(name)}`
1084
+ );
1085
+ }
1086
+ }
1087
+ function isPlainObject(value) {
1088
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1089
+ const proto = Object.getPrototypeOf(value);
1090
+ return proto === Object.prototype || proto === null;
1091
+ }
1092
+ function isCallToolResult(value) {
1093
+ if (!isPlainObject(value) || !Array.isArray(value.content)) return false;
1094
+ return value.content.every(
1095
+ (block) => isPlainObject(block) && typeof block.type === "string"
1096
+ );
1097
+ }
1098
+ function toCallToolResult(value) {
1099
+ if (isCallToolResult(value)) return value;
1100
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? null, null, 2);
1101
+ const result = { content: [{ type: "text", text }] };
1102
+ if (isPlainObject(value)) result.structuredContent = value;
1103
+ return result;
1104
+ }
1105
+ function issuePath(issue) {
1106
+ return issue.path.map(String).join(".") || "(root)";
1107
+ }
1108
+ function invalidInputError(issues) {
1109
+ const fields = [...new Set(issues.map(issuePath))];
1110
+ const summary = issues.map((issue) => `${issuePath(issue)}: ${issue.message}`).join("; ");
1111
+ return toolError({
1112
+ code: "INVALID_INPUT",
1113
+ cause: `invalid input for ${fields.join(", ")} \u2014 ${summary}`,
1114
+ fix: `fix ${fields.join(", ")} to match the tool's inputSchema (see details.issues), then retry`,
1115
+ retryable: false,
1116
+ details: {
1117
+ fields,
1118
+ issues: issues.map((issue) => ({
1119
+ path: issue.path.map(String),
1120
+ code: issue.code,
1121
+ message: issue.message
1122
+ }))
1123
+ }
1124
+ });
1125
+ }
1126
+ function defineTool(opts) {
1127
+ assertToolName(opts.name);
1128
+ if (typeof opts.description !== "string" || opts.description.trim().length < MIN_DESCRIPTION_LENGTH) {
1129
+ throw new Error(
1130
+ `tool ${opts.name}: description must be at least ${MIN_DESCRIPTION_LENGTH} characters \u2014 say what it does, when to use it and what it returns`
1131
+ );
1132
+ }
1133
+ const { name, title, description, input, output, annotations } = opts;
1134
+ const handler = opts.handler.bind(opts);
1135
+ const tool = {
1136
+ name,
1137
+ title,
1138
+ description,
1139
+ input,
1140
+ output,
1141
+ annotations,
1142
+ handler,
1143
+ async invoke(rawInput, ctx) {
1144
+ const fail2 = (err) => {
1145
+ const result = errorResult(err, { requestId: ctx.requestId });
1146
+ if (output) delete result.structuredContent;
1147
+ return result;
1148
+ };
1149
+ const parsed = input.safeParse(rawInput ?? {});
1150
+ if (!parsed.success) return fail2(invalidInputError(parsed.error.issues));
1151
+ try {
1152
+ const value = await handler(parsed.data, ctx);
1153
+ if (output && !isCallToolResult(value)) {
1154
+ const checked = output.safeParse(value);
1155
+ if (!checked.success) {
1156
+ throw toolError({
1157
+ code: "INTERNAL",
1158
+ cause: `tool ${name} returned a value that does not match its output schema \u2014 ${checked.error.issues.map((issue) => `${issuePath(issue)}: ${issue.message}`).join("; ")}`,
1159
+ fix: "this is a bug in the tool, not in your call; report it with the request id",
1160
+ retryable: false,
1161
+ details: { issues: checked.error.issues }
1162
+ });
1163
+ }
1164
+ return toCallToolResult(checked.data);
1165
+ }
1166
+ return toCallToolResult(value);
1167
+ } catch (err) {
1168
+ return fail2(err);
1169
+ }
1170
+ }
1171
+ };
1172
+ return tool;
1173
+ }
1174
+ var PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
1175
+ var WAV_BASE64 = "UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=";
1176
+ var STATIC_TEXT_URI = "test://static-text";
1177
+ var STATIC_TEXT = "This is a static text resource for testing.";
1178
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
1179
+ function embeddedResource() {
1180
+ return {
1181
+ type: "resource",
1182
+ resource: { uri: STATIC_TEXT_URI, mimeType: "text/plain", text: STATIC_TEXT }
1183
+ };
1184
+ }
1185
+ function requireExtra(ctx) {
1186
+ if (!ctx.extra) {
1187
+ throw toolError("INTERNAL", "no client connection in this context", "call this tool over MCP");
1188
+ }
1189
+ return ctx.extra;
1190
+ }
1191
+ async function elicit(ctx, message, requestedSchema) {
1192
+ const result = await requireExtra(ctx).sendRequest(
1193
+ { method: "elicitation/create", params: { message, requestedSchema } },
1194
+ ElicitResultSchema
1195
+ );
1196
+ return `Elicitation completed: action=${result.action}, content=${JSON.stringify(result.content ?? {})}`;
1197
+ }
1198
+ var conformanceTools = [
1199
+ defineTool({
1200
+ name: "test_simple_text",
1201
+ description: "Conformance fixture: returns a fixed simple text response.",
1202
+ input: z.object({}),
1203
+ handler: () => "This is a simple text response for testing."
1204
+ }),
1205
+ defineTool({
1206
+ name: "test_error_handling",
1207
+ description: "Conformance fixture: always returns an error result (isError: true).",
1208
+ input: z.object({}),
1209
+ handler: () => {
1210
+ throw toolError({
1211
+ code: "UPSTREAM_ERROR",
1212
+ cause: "This tool intentionally returns an error for testing",
1213
+ fix: "nothing to fix; this fixture always fails",
1214
+ retryable: false
1215
+ });
1216
+ }
1217
+ }),
1218
+ defineTool({
1219
+ name: "test_image_content",
1220
+ description: "Conformance fixture: returns a 1x1 PNG image content block.",
1221
+ input: z.object({}),
1222
+ handler: () => ({ content: [{ type: "image", data: PNG_BASE64, mimeType: "image/png" }] })
1223
+ }),
1224
+ defineTool({
1225
+ name: "test_audio_content",
1226
+ description: "Conformance fixture: returns an empty WAV audio content block.",
1227
+ input: z.object({}),
1228
+ handler: () => ({ content: [{ type: "audio", data: WAV_BASE64, mimeType: "audio/wav" }] })
1229
+ }),
1230
+ defineTool({
1231
+ name: "test_embedded_resource",
1232
+ description: "Conformance fixture: returns an embedded text resource content block.",
1233
+ input: z.object({}),
1234
+ handler: () => ({ content: [embeddedResource()] })
1235
+ }),
1236
+ defineTool({
1237
+ name: "test_multiple_content_types",
1238
+ description: "Conformance fixture: returns text, image and resource content blocks together.",
1239
+ input: z.object({}),
1240
+ handler: () => ({
1241
+ content: [
1242
+ { type: "text", text: "Mixed content response" },
1243
+ { type: "image", data: PNG_BASE64, mimeType: "image/png" },
1244
+ embeddedResource()
1245
+ ]
1246
+ })
1247
+ }),
1248
+ defineTool({
1249
+ name: "test_tool_with_logging",
1250
+ description: "Conformance fixture: emits three info log notifications while running.",
1251
+ input: z.object({}),
1252
+ handler: async (_input, ctx) => {
1253
+ await ctx.log?.("info", "Tool execution started");
1254
+ await sleep(50);
1255
+ await ctx.log?.("info", "Tool processing data");
1256
+ await sleep(50);
1257
+ await ctx.log?.("info", "Tool execution completed");
1258
+ return "Tool execution completed with logging";
1259
+ }
1260
+ }),
1261
+ defineTool({
1262
+ name: "test_tool_with_progress",
1263
+ description: "Conformance fixture: reports progress 0/100, 50/100, 100/100 while running.",
1264
+ input: z.object({}),
1265
+ handler: async (_input, ctx) => {
1266
+ await ctx.progress?.(0, 100, "starting");
1267
+ await sleep(50);
1268
+ await ctx.progress?.(50, 100, "halfway");
1269
+ await sleep(50);
1270
+ await ctx.progress?.(100, 100, "done");
1271
+ return "Tool execution completed with progress";
1272
+ }
1273
+ }),
1274
+ defineTool({
1275
+ name: "test_sampling",
1276
+ description: "Conformance fixture: asks the client to sample a completion and returns it.",
1277
+ input: z.object({ prompt: z.string().describe("Prompt to send to the client's model") }),
1278
+ handler: async ({ prompt }, ctx) => {
1279
+ const result = await requireExtra(ctx).sendRequest(
1280
+ {
1281
+ method: "sampling/createMessage",
1282
+ params: {
1283
+ messages: [{ role: "user", content: { type: "text", text: prompt } }],
1284
+ maxTokens: 100
1285
+ }
1286
+ },
1287
+ CreateMessageResultSchema
1288
+ );
1289
+ const content = Array.isArray(result.content) ? result.content[0] : result.content;
1290
+ const text = content && content.type === "text" ? content.text : JSON.stringify(content);
1291
+ return `Sampling completed (${result.model}): ${text}`;
1292
+ }
1293
+ }),
1294
+ defineTool({
1295
+ name: "test_elicitation",
1296
+ description: "Conformance fixture: asks the client for user input and reports the outcome.",
1297
+ input: z.object({ message: z.string().describe("Message shown to the user") }),
1298
+ handler: ({ message }, ctx) => elicit(ctx, message, {
1299
+ type: "object",
1300
+ properties: {
1301
+ name: { type: "string", title: "Name", description: "Your name" },
1302
+ email: { type: "string", title: "Email", format: "email" }
1303
+ },
1304
+ required: ["name"]
1305
+ })
1306
+ }),
1307
+ defineTool({
1308
+ name: "test_elicitation_sep1034_defaults",
1309
+ description: "Conformance fixture: elicitation schema with defaults for every primitive type.",
1310
+ input: z.object({}),
1311
+ handler: (_input, ctx) => elicit(ctx, "Please confirm your details", {
1312
+ type: "object",
1313
+ properties: {
1314
+ name: { type: "string", title: "Name", default: "John Doe" },
1315
+ age: { type: "integer", title: "Age", default: 30 },
1316
+ score: { type: "number", title: "Score", default: 95.5 },
1317
+ status: {
1318
+ type: "string",
1319
+ title: "Status",
1320
+ enum: ["active", "inactive", "pending"],
1321
+ default: "active"
1322
+ },
1323
+ verified: { type: "boolean", title: "Verified", default: true }
1324
+ }
1325
+ })
1326
+ }),
1327
+ defineTool({
1328
+ name: "test_elicitation_sep1330_enums",
1329
+ description: "Conformance fixture: elicitation schema with titled, untitled and multi-select enums.",
1330
+ input: z.object({}),
1331
+ handler: (_input, ctx) => elicit(ctx, "Pick some options", {
1332
+ type: "object",
1333
+ properties: {
1334
+ untitledSingle: {
1335
+ type: "string",
1336
+ title: "Untitled single",
1337
+ enum: ["red", "green", "blue"]
1338
+ },
1339
+ titledSingle: {
1340
+ type: "string",
1341
+ title: "Titled single",
1342
+ oneOf: [
1343
+ { const: "red", title: "Red" },
1344
+ { const: "green", title: "Green" },
1345
+ { const: "blue", title: "Blue" }
1346
+ ]
1347
+ },
1348
+ legacyEnum: {
1349
+ type: "string",
1350
+ title: "Legacy titled",
1351
+ enum: ["red", "green", "blue"],
1352
+ enumNames: ["Red", "Green", "Blue"]
1353
+ },
1354
+ untitledMulti: {
1355
+ type: "array",
1356
+ title: "Untitled multi",
1357
+ items: { type: "string", enum: ["red", "green", "blue"] }
1358
+ },
1359
+ titledMulti: {
1360
+ type: "array",
1361
+ title: "Titled multi",
1362
+ items: {
1363
+ anyOf: [
1364
+ { const: "red", title: "Red" },
1365
+ { const: "green", title: "Green" },
1366
+ { const: "blue", title: "Blue" }
1367
+ ]
1368
+ }
1369
+ }
1370
+ }
1371
+ })
1372
+ }),
1373
+ defineTool({
1374
+ name: "json_schema_2020_12_tool",
1375
+ description: "Conformance fixture: input schema with a $defs reference to an address object.",
1376
+ input: z.object({
1377
+ name: z.string(),
1378
+ address: z.object({ street: z.string(), city: z.string() }).meta({ id: "address" })
1379
+ }),
1380
+ handler: (input) => input
1381
+ })
1382
+ ];
1383
+ function paymentRequiredError(body, reason) {
1384
+ const { code, cause, fix, retryable, ...rest } = body;
1385
+ return toolError({
1386
+ code,
1387
+ cause,
1388
+ fix,
1389
+ retryable,
1390
+ httpStatus: 402,
1391
+ details: reason !== void 0 ? { reason } : void 0,
1392
+ extra: rest
1393
+ });
1394
+ }
1395
+ function formatUsd(priceUsd) {
1396
+ return priceUsd.toFixed(6).replace(/0+$/, "").replace(/\.$/, ".00").replace(/\.(\d)$/, ".$10");
1397
+ }
1398
+ function withPayment(tool, opts) {
1399
+ if (!Number.isFinite(opts.priceUsd) || opts.priceUsd < 0) {
1400
+ throw new Error(`withPayment(${tool.name}): priceUsd must be a non-negative number`);
868
1401
  }
869
- get policy() {
870
- return this.loaded.policy;
1402
+ const sampleField = opts.sampleField ?? "sample";
1403
+ if (sampleField in tool.input.shape) {
1404
+ throw new Error(
1405
+ `withPayment(${tool.name}): input already has a field named ${JSON.stringify(sampleField)}; pass sampleField to rename it`
1406
+ );
871
1407
  }
872
- async start() {
873
- await Promise.all(this.policy.upstreams.map((u) => this.connect(u)));
874
- for (const [name, client] of Object.entries(this.testClients)) {
875
- if (this.connections.has(name)) continue;
876
- const upstream = {
877
- name,
878
- command: "in-memory",
879
- args: [],
880
- env: {},
881
- headers: {},
882
- prefix: false,
883
- timeout_ms: 6e4
884
- };
885
- const conn = {
886
- upstream,
887
- client,
888
- tools: [],
889
- capabilities: client.getServerCapabilities(),
890
- connected: true
1408
+ const verifier = opts.verifier ?? new StubVerifier();
1409
+ const liveRail = opts.verifier !== void 0;
1410
+ const input = tool.input.extend({
1411
+ [sampleField]: z2.boolean().default(false).describe(
1412
+ "Set true to return an example response at no charge (sample mode). Default false."
1413
+ )
1414
+ });
1415
+ const description = `${tool.description.trim()} Paid: $${formatUsd(opts.priceUsd)} per call; without credit you get a PAYMENT_REQUIRED result. Set ${sampleField}=true for a free example response.`;
1416
+ return defineTool({
1417
+ name: tool.name,
1418
+ title: tool.title,
1419
+ description,
1420
+ input,
1421
+ output: tool.output,
1422
+ annotations: tool.annotations,
1423
+ async handler(raw, ctx) {
1424
+ const record = { ...raw };
1425
+ const sample = record[sampleField] === true;
1426
+ delete record[sampleField];
1427
+ if (sample) return tool.handler(record, { ...ctx, sample: true });
1428
+ let reason;
1429
+ if (opts.authorize) {
1430
+ const auth = await opts.authorize(ctx);
1431
+ if (auth.ok) return tool.handler(record, { ...ctx, userId: auth.userId });
1432
+ reason = auth.reason;
1433
+ }
1434
+ const resource = ctx.resourceUrl ?? tool.name;
1435
+ const required = buildPaymentRequired({
1436
+ resource,
1437
+ priceUsd: opts.priceUsd,
1438
+ live: opts.live,
1439
+ description: opts.description,
1440
+ payTo: opts.payTo,
1441
+ creditsPayTo: opts.creditsPayTo,
1442
+ signupUrl: opts.signupUrl,
1443
+ selfServe: opts.selfServe,
1444
+ // withPayment always adds a free `sample` flag to the tool's input schema
1445
+ sampleMode: true,
1446
+ ...opts.bazaar ? { bazaar: opts.bazaar } : {}
1447
+ });
1448
+ const advertised = required.body.accepts.find((a) => a.rail === "prepaid_credits") ?? required.body.accepts[0];
1449
+ const requirement = advertised ?? {
1450
+ rail: "prepaid_credits",
1451
+ amountUsd: String(required.body.priceUsd),
1452
+ resource,
1453
+ ...opts.description ? { description: opts.description } : {}
891
1454
  };
892
- this.connections.set(name, conn);
893
- this.wire(conn);
894
- await this.refreshTools(name);
1455
+ const verified = await verifier.verify(
1456
+ new Request(`https://mcp.local/${tool.name}`, { headers: ctx.headers }),
1457
+ requirement
1458
+ );
1459
+ if (verified.ok) {
1460
+ let result;
1461
+ try {
1462
+ result = await tool.handler(record, ctx);
1463
+ } catch (error) {
1464
+ await opts.onPaidFailure?.(ctx, verified, error);
1465
+ throw error;
1466
+ }
1467
+ const settled = await verifier.settle?.(verified);
1468
+ if (settled && !settled.ok) {
1469
+ opts.onSettlementFailure?.(ctx, verified, settled.reason);
1470
+ }
1471
+ return result;
1472
+ }
1473
+ reason = liveRail ? verified.reason ?? reason : reason ?? verified.reason;
1474
+ throw paymentRequiredError(required.body, reason);
1475
+ }
1476
+ });
1477
+ }
1478
+ var exampleEcho = defineTool({
1479
+ name: "example_echo",
1480
+ title: "Echo",
1481
+ description: "Echo a message back unchanged. Use it to check the connection and to see the result shape mcp-kit tools return (JSON text plus structuredContent).",
1482
+ input: z3.object({ message: z3.string().describe("Text to echo back") }),
1483
+ output: z3.object({ message: z3.string() }),
1484
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
1485
+ handler: ({ message }) => ({ message })
1486
+ });
1487
+ var exampleAdd = defineTool({
1488
+ name: "example_add",
1489
+ title: "Add",
1490
+ description: "Add two numbers and return their sum as { sum }. Demonstrates typed input and output schemas; send non-numbers to see an INVALID_INPUT error result.",
1491
+ input: z3.object({
1492
+ a: z3.number().describe("First addend"),
1493
+ b: z3.number().describe("Second addend")
1494
+ }),
1495
+ output: z3.object({ sum: z3.number() }),
1496
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
1497
+ handler: ({ a, b }) => ({ sum: a + b })
1498
+ });
1499
+ var examplePaidLookup = withPayment(
1500
+ defineTool({
1501
+ name: "example_paid_lookup",
1502
+ title: "Paid lookup",
1503
+ description: "Look up a record by query and return { query, result, sample } (fake data). Demonstrates a paid tool: without an entitlement the call returns a PAYMENT_REQUIRED result with x402 and MPP payment options.",
1504
+ input: z3.object({ query: z3.string().min(1).describe("What to look up") }),
1505
+ output: z3.object({ query: z3.string(), result: z3.string(), sample: z3.boolean() }),
1506
+ annotations: { readOnlyHint: true, idempotentHint: true },
1507
+ handler: ({ query }, ctx) => ({
1508
+ query,
1509
+ result: ctx.sample ? `SAMPLE: example record for "${query}"` : `record for "${query}"`,
1510
+ sample: ctx.sample === true
1511
+ })
1512
+ }),
1513
+ { priceUsd: 0.05, description: "One record lookup" }
1514
+ );
1515
+ function isZodObject(schema) {
1516
+ return schema instanceof z4.ZodObject;
1517
+ }
1518
+ function contextFromExtra(extra) {
1519
+ const headers = new Headers();
1520
+ const raw = extra.requestInfo?.headers;
1521
+ if (raw) {
1522
+ for (const [key, value] of Object.entries(raw)) {
1523
+ if (Array.isArray(value)) for (const item of value) headers.append(key, item);
1524
+ else if (typeof value === "string") headers.append(key, value);
895
1525
  }
896
- this.rebuildExposed();
897
- const status = this.upstreamStatus();
898
- this.log(
899
- `connected ${status.filter((s) => s.connected).length}/${status.length} upstreams, ${this.exposed.length} tools, mode ${this.policy.mode}`
900
- );
901
1526
  }
902
- async connect(upstream) {
903
- const client = new Client(
904
- { name: "agentguard", version: version() },
905
- { capabilities: { sampling: {}, elicitation: {} } }
906
- );
907
- const conn = {
908
- upstream,
909
- client,
910
- tools: [],
911
- capabilities: void 0,
912
- connected: false
913
- };
914
- this.connections.set(upstream.name, conn);
915
- try {
916
- const transport = upstream.url ? new StreamableHTTPClientTransport(new URL(upstream.url), {
917
- requestInit: { headers: upstreamHeaders(upstream) }
918
- }) : new StdioClientTransport({
919
- command: upstream.command,
920
- args: upstream.args,
921
- env: { ...getDefaultEnvironment(), ...upstream.env },
922
- cwd: upstream.cwd,
923
- stderr: "pipe"
1527
+ const progressToken = extra._meta?.progressToken;
1528
+ return {
1529
+ headers,
1530
+ requestId: String(extra.requestId),
1531
+ signal: extra.signal,
1532
+ sessionId: extra.sessionId,
1533
+ meta: extra._meta,
1534
+ extra,
1535
+ log: async (level, data) => {
1536
+ await extra.sendNotification({ method: "notifications/message", params: { level, data } });
1537
+ },
1538
+ progress: async (progress, total, message) => {
1539
+ if (progressToken === void 0) return;
1540
+ await extra.sendNotification({
1541
+ method: "notifications/progress",
1542
+ params: { progressToken, progress, total, message }
924
1543
  });
925
- if (transport instanceof StdioClientTransport) {
926
- transport.stderr?.on("data", (chunk) => {
927
- if (this.closing) return;
928
- for (const line of chunk.toString("utf8").split("\n"))
929
- if (line.trim()) this.log(`${upstream.name}: ${line}`);
930
- });
931
- }
932
- await withTimeout(
933
- client.connect(transport),
934
- this.connectTimeoutMs,
935
- `connect to upstream "${upstream.name}"`
936
- );
937
- conn.capabilities = client.getServerCapabilities();
938
- conn.connected = true;
939
- this.wire(conn);
940
- await this.refreshTools(upstream.name);
941
- } catch (err) {
942
- conn.connected = false;
943
- conn.error = err instanceof Error ? err.message : String(err);
944
- this.log(`upstream "${upstream.name}" unavailable: ${conn.error}`);
945
1544
  }
1545
+ };
1546
+ }
1547
+ function createMcpServer(opts) {
1548
+ const byName = /* @__PURE__ */ new Map();
1549
+ for (const tool of opts.tools) {
1550
+ if (byName.has(tool.name)) throw new Error(`duplicate tool name: ${tool.name}`);
1551
+ byName.set(tool.name, tool);
946
1552
  }
947
- wire(conn) {
948
- const { client, upstream } = conn;
949
- client.onclose = () => {
950
- conn.connected = false;
951
- conn.error = "connection closed";
952
- if (!this.closing) this.log(`upstream "${upstream.name}" closed`);
1553
+ const server = new McpServer(
1554
+ { name: opts.name, version: opts.version, title: opts.title, websiteUrl: opts.websiteUrl },
1555
+ { instructions: opts.instructions, capabilities: { logging: {}, ...opts.capabilities } }
1556
+ );
1557
+ const registered = /* @__PURE__ */ new Map();
1558
+ for (const tool of byName.values()) {
1559
+ const config = {
1560
+ title: tool.title,
1561
+ description: tool.description,
1562
+ inputSchema: tool.input,
1563
+ annotations: tool.annotations
953
1564
  };
954
- client.setNotificationHandler(LoggingMessageNotificationSchema, async (n) => {
955
- await this.broadcast(upstream.name, n.method, n.params);
956
- });
957
- client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (n) => {
958
- await this.broadcast(upstream.name, n.method, n.params);
959
- });
960
- client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
961
- this.resourceOwner.clear();
962
- await this.broadcast(upstream.name, "notifications/resources/list_changed", void 0);
963
- });
964
- client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
965
- this.promptOwner.clear();
966
- await this.broadcast(upstream.name, "notifications/prompts/list_changed", void 0);
1565
+ if (isZodObject(tool.output)) config.outputSchema = tool.output;
1566
+ registered.set(
1567
+ tool.name,
1568
+ server.registerTool(tool.name, config, () => {
1569
+ throw new Error("unreachable: mcp-kit dispatches tools/call itself");
1570
+ })
1571
+ );
1572
+ }
1573
+ if (byName.size > 0) {
1574
+ server.server.setRequestHandler(ListToolsRequestSchema, async () => {
1575
+ const enabled = [...byName.values()].filter(
1576
+ (tool) => registered.get(tool.name)?.enabled !== false
1577
+ );
1578
+ return {
1579
+ tools: listToolManifest(enabled).map((entry) => ({
1580
+ ...entry,
1581
+ inputSchema: entry.inputSchema,
1582
+ outputSchema: entry.outputSchema
1583
+ }))
1584
+ };
967
1585
  });
968
- client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
969
- await this.refreshTools(upstream.name);
970
- this.rebuildExposed();
971
- for (const s of this.sessions) s.server.server.sendToolListChanged().catch(() => void 0);
1586
+ server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1587
+ const name = request.params.name;
1588
+ const tool = byName.get(name);
1589
+ const entry = registered.get(name);
1590
+ if (!tool || !entry) throw new McpError(ErrorCode.InvalidParams, `Tool ${name} not found`);
1591
+ if (!entry.enabled) throw new McpError(ErrorCode.InvalidParams, `Tool ${name} disabled`);
1592
+ return tool.invoke(request.params.arguments ?? {}, contextFromExtra(extra));
972
1593
  });
973
- try {
974
- client.setRequestHandler(CreateMessageRequestSchema, async (req) => {
975
- const session = this.pickSession(upstream.name);
976
- if (!session) throw new Error("no downstream client is connected to relay sampling to");
977
- const related = session.inflight.get(upstream.name)?.at(-1);
978
- return session.server.server.createMessage(req.params, { relatedRequestId: related });
979
- });
980
- client.setRequestHandler(ElicitRequestSchema, async (req) => {
981
- const session = this.pickSession(upstream.name);
982
- if (!session) throw new Error("no downstream client is connected to relay elicitation to");
983
- const related = session.inflight.get(upstream.name)?.at(-1);
984
- return session.server.server.elicitInput(req.params, { relatedRequestId: related });
985
- });
986
- } catch {
987
- }
988
1594
  }
989
- pickSession(upstream) {
990
- let best;
991
- for (const s of this.sessions) {
992
- if ((s.inflight.get(upstream)?.length ?? 0) > 0) return s;
993
- if (!best || s.lastActive > best.lastActive) best = s;
1595
+ return server;
1596
+ }
1597
+ function toJsonSchema(schema, io = "input") {
1598
+ const json2 = z4.toJSONSchema(schema, { io, target: "draft-2020-12" });
1599
+ if (isZodObject(schema) && schema._zod.def.catchall === void 0 && json2.additionalProperties === void 0) {
1600
+ json2.additionalProperties = false;
1601
+ }
1602
+ return json2;
1603
+ }
1604
+ function listToolManifest(tools) {
1605
+ return tools.map((tool) => {
1606
+ const entry = {
1607
+ name: tool.name,
1608
+ description: tool.description,
1609
+ inputSchema: toJsonSchema(tool.input, "input")
1610
+ };
1611
+ if (tool.title !== void 0) entry.title = tool.title;
1612
+ if (isZodObject(tool.output)) entry.outputSchema = toJsonSchema(tool.output, "output");
1613
+ if (tool.annotations !== void 0) entry.annotations = tool.annotations;
1614
+ return entry;
1615
+ });
1616
+ }
1617
+ var DEFAULT_CORS = {
1618
+ origin: "*",
1619
+ methods: "GET, POST, DELETE, OPTIONS",
1620
+ headers: "Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, PAYMENT, X-PAYMENT",
1621
+ exposeHeaders: "Mcp-Session-Id, Mcp-Protocol-Version, WWW-Authenticate, X-Payment-Requirements, PAYMENT-REQUIRED",
1622
+ maxAge: 86400
1623
+ };
1624
+ var DEFAULT_SESSION_TTL_MS = 30 * 6e4;
1625
+ function corsHeaders(cors) {
1626
+ if (!cors) return {};
1627
+ const c = { ...DEFAULT_CORS, ...typeof cors === "object" ? cors : {} };
1628
+ return {
1629
+ "Access-Control-Allow-Origin": c.origin,
1630
+ "Access-Control-Allow-Methods": c.methods,
1631
+ "Access-Control-Allow-Headers": c.headers,
1632
+ "Access-Control-Expose-Headers": c.exposeHeaders,
1633
+ "Access-Control-Max-Age": String(c.maxAge)
1634
+ };
1635
+ }
1636
+ function jsonResponse(body, status, headers = {}) {
1637
+ return new Response(JSON.stringify(body), {
1638
+ status,
1639
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-store", ...headers }
1640
+ });
1641
+ }
1642
+ function methodNotAllowed(method, allow, cors) {
1643
+ return jsonResponse(
1644
+ {
1645
+ code: "METHOD_NOT_ALLOWED",
1646
+ cause: `${method} is not supported here without a session`,
1647
+ fix: "send JSON-RPC over POST with `Accept: application/json, text/event-stream` (start with `initialize`)",
1648
+ retryable: false
1649
+ },
1650
+ 405,
1651
+ { Allow: allow, ...cors }
1652
+ );
1653
+ }
1654
+ function splitOptions(opts, transport) {
1655
+ if (typeof opts === "function") return { factory: opts, transport };
1656
+ const {
1657
+ cors,
1658
+ jsonResponse: jsonResponse2,
1659
+ sessions,
1660
+ allowedHosts,
1661
+ allowedOrigins,
1662
+ enableDnsRebindingProtection,
1663
+ ...server
1664
+ } = opts;
1665
+ return {
1666
+ factory: () => createMcpServer(server),
1667
+ transport: {
1668
+ cors,
1669
+ jsonResponse: jsonResponse2,
1670
+ sessions,
1671
+ allowedHosts,
1672
+ allowedOrigins,
1673
+ enableDnsRebindingProtection,
1674
+ ...transport
994
1675
  }
995
- return best;
996
- }
997
- async broadcast(upstream, method, params) {
998
- for (const s of this.sessions) {
999
- const related = s.inflight.get(upstream)?.at(-1);
1676
+ };
1677
+ }
1678
+ function withCors(response, cors) {
1679
+ const keys = Object.keys(cors);
1680
+ if (keys.length === 0) return response;
1681
+ const headers = new Headers(response.headers);
1682
+ for (const key of keys) headers.set(key, cors[key] ?? "");
1683
+ return new Response(response.body, {
1684
+ status: response.status,
1685
+ statusText: response.statusText,
1686
+ headers
1687
+ });
1688
+ }
1689
+ function errorResponse(err, cors) {
1690
+ const error = normalizeError(err);
1691
+ return jsonResponse(error.toJSON(), error.httpStatus, cors);
1692
+ }
1693
+ function createStatelessHandler({ factory, cors, transportBase }) {
1694
+ return async (request) => {
1695
+ if (request.method !== "POST") return methodNotAllowed(request.method, "POST, OPTIONS", cors);
1696
+ const server = factory();
1697
+ const transport = new WebStandardStreamableHTTPServerTransport({
1698
+ ...transportBase,
1699
+ sessionIdGenerator: void 0
1700
+ });
1701
+ let closed = false;
1702
+ const cleanup = async () => {
1703
+ if (closed) return;
1704
+ closed = true;
1000
1705
  try {
1001
- await s.server.server.notification(
1002
- { method, params },
1003
- related !== void 0 ? { relatedRequestId: related } : void 0
1004
- );
1706
+ await server.close();
1005
1707
  } catch {
1006
1708
  }
1007
- }
1008
- }
1009
- async refreshTools(name) {
1010
- const conn = this.connections.get(name);
1011
- if (!conn?.connected) return;
1012
- if (conn.capabilities && !conn.capabilities.tools) {
1013
- conn.tools = [];
1014
- return;
1015
- }
1709
+ };
1016
1710
  try {
1017
- const tools = [];
1018
- let cursor;
1019
- do {
1020
- const page = await conn.client.listTools(cursor ? { cursor } : void 0, {
1021
- timeout: conn.upstream.timeout_ms
1022
- });
1023
- tools.push(...page.tools);
1024
- cursor = page.nextCursor;
1025
- } while (cursor);
1026
- conn.tools = tools;
1027
- } catch (err) {
1028
- this.log(
1029
- `tools/list failed for "${name}": ${err instanceof Error ? err.message : String(err)}`
1030
- );
1031
- conn.tools = [];
1032
- }
1033
- }
1034
- rebuildExposed() {
1035
- const taken = /* @__PURE__ */ new Set();
1036
- const out = [];
1037
- const order = [...this.policy.upstreams.map((u) => u.name), ...Object.keys(this.testClients)];
1038
- for (const name of order) {
1039
- const conn = this.connections.get(name);
1040
- if (!conn) continue;
1041
- const prefix = conn.upstream.prefix === true ? name : typeof conn.upstream.prefix === "string" ? conn.upstream.prefix : void 0;
1042
- for (const tool of conn.tools) {
1043
- let exposedName = prefix ? `${prefix}__${tool.name}` : tool.name;
1044
- if (taken.has(exposedName)) exposedName = `${name}__${tool.name}`;
1045
- if (taken.has(exposedName)) {
1046
- this.log(`skipping duplicate tool ${exposedName} from ${name}`);
1047
- continue;
1048
- }
1049
- taken.add(exposedName);
1050
- out.push({
1051
- name: exposedName,
1052
- upstream: name,
1053
- original: tool.name,
1054
- tool,
1055
- classification: classifyTool(
1056
- {
1057
- name: exposedName,
1058
- description: tool.description,
1059
- annotations: tool.annotations,
1060
- inputSchema: tool.inputSchema,
1061
- outputSchema: tool.outputSchema
1062
- },
1063
- this.policy
1064
- )
1065
- });
1711
+ await server.connect(transport);
1712
+ const response = withCors(await transport.handleRequest(request), cors);
1713
+ if (!response.body) {
1714
+ await cleanup();
1715
+ return response;
1066
1716
  }
1067
- }
1068
- this.exposed = out;
1069
- }
1070
- listTools(agent) {
1071
- if (agent?.upstreams) return this.exposed.filter((t) => agent.upstreams.includes(t.upstream));
1072
- return this.exposed;
1073
- }
1074
- findTool(name) {
1075
- return this.exposed.find((t) => t.name === name);
1076
- }
1077
- upstreamStatus() {
1078
- return [...this.connections.values()].map((c) => ({
1079
- name: c.upstream.name,
1080
- connected: c.connected,
1081
- tools: c.tools.length,
1082
- transport: c.upstream.url ? "http" : "stdio",
1083
- error: c.error
1084
- }));
1085
- }
1086
- attach(session) {
1087
- this.sessions.add(session);
1088
- }
1089
- detach(session) {
1090
- this.sessions.delete(session);
1091
- }
1092
- /** Run a tool call through the policy engine and, if allowed, the upstream. */
1093
- async callTool(name, args, ctx) {
1094
- const tool = this.findTool(name);
1095
- const agent = ctx.agent === void 0 ? this.agent : ctx.agent;
1096
- if (!tool) {
1097
- const result2 = await this.guard.run(
1098
- { tool: { name }, args, runId: ctx.runId, agent, sessionId: ctx.sessionId },
1099
- async () => {
1100
- throw new GuardError({
1101
- code: "UNKNOWN_TOOL",
1102
- cause: `no upstream exposes a tool named "${name}"`,
1103
- fix: "call tools/list and use one of the listed names",
1104
- retryable: false
1105
- });
1106
- }
1717
+ request.signal?.addEventListener("abort", () => void cleanup(), { once: true });
1718
+ const body = response.body.pipeThrough(
1719
+ new TransformStream({ flush: () => cleanup() })
1107
1720
  );
1108
- return { result: result2 };
1721
+ return new Response(body, {
1722
+ status: response.status,
1723
+ statusText: response.statusText,
1724
+ headers: response.headers
1725
+ });
1726
+ } catch (err) {
1727
+ await cleanup();
1728
+ return errorResponse(err, cors);
1109
1729
  }
1110
- const conn = this.connections.get(tool.upstream);
1111
- const track = () => {
1112
- if (!ctx.session || ctx.requestId === void 0) return () => void 0;
1113
- const list = ctx.session.inflight.get(tool.upstream) ?? [];
1114
- list.push(ctx.requestId);
1115
- ctx.session.inflight.set(tool.upstream, list);
1116
- return () => {
1117
- const l = ctx.session.inflight.get(tool.upstream) ?? [];
1118
- const i = l.indexOf(ctx.requestId);
1119
- if (i >= 0) l.splice(i, 1);
1120
- };
1121
- };
1122
- const result = await this.guard.run(
1123
- {
1124
- tool: {
1125
- name: tool.name,
1126
- description: tool.tool.description,
1127
- annotations: tool.tool.annotations,
1128
- inputSchema: tool.tool.inputSchema,
1129
- outputSchema: tool.tool.outputSchema
1130
- },
1131
- args,
1132
- runId: ctx.runId,
1133
- agent,
1134
- sessionId: ctx.sessionId,
1135
- upstream: tool.upstream
1136
- },
1137
- async (finalArgs) => {
1138
- if (!conn?.connected) {
1139
- throw new GuardError({
1140
- code: "UPSTREAM_ERROR",
1141
- cause: `upstream "${tool.upstream}" is not connected${conn?.error ? ` (${conn.error})` : ""}`,
1142
- fix: "check the upstream command/url in agentguard.yaml and restart the proxy",
1143
- retryable: true
1144
- });
1145
- }
1146
- const untrack = track();
1147
- try {
1148
- return await conn.client.callTool(
1149
- { name: tool.original, arguments: finalArgs ?? {} },
1150
- void 0,
1151
- {
1152
- timeout: conn.upstream.timeout_ms,
1153
- resetTimeoutOnProgress: true,
1154
- onprogress: ctx.onprogress
1155
- }
1156
- );
1157
- } finally {
1158
- untrack();
1159
- }
1730
+ };
1731
+ }
1732
+ function createStatefulHandler({ factory, cors, transportBase }, options) {
1733
+ const ttlMs = options.ttlMs ?? DEFAULT_SESSION_TTL_MS;
1734
+ const sessions = /* @__PURE__ */ new Map();
1735
+ const drop = (id) => {
1736
+ const session = sessions.get(id);
1737
+ if (!session) return;
1738
+ sessions.delete(id);
1739
+ session.server.close().catch(() => void 0);
1740
+ };
1741
+ const sweep = () => {
1742
+ const now = Date.now();
1743
+ for (const [id, session] of sessions) if (now - session.lastSeen > ttlMs) drop(id);
1744
+ };
1745
+ return async (request) => {
1746
+ const sessionId = request.headers.get("mcp-session-id");
1747
+ if (sessionId) {
1748
+ const session = sessions.get(sessionId);
1749
+ if (!session) {
1750
+ return jsonResponse(
1751
+ {
1752
+ code: "NOT_FOUND",
1753
+ cause: "unknown or expired Mcp-Session-Id",
1754
+ fix: "send `initialize` again without an Mcp-Session-Id header and use the id from the response",
1755
+ retryable: true
1756
+ },
1757
+ 404,
1758
+ cors
1759
+ );
1160
1760
  }
1161
- );
1162
- return { result, tool };
1163
- }
1164
- connectedWith(capability) {
1165
- return [...this.connections.values()].filter(
1166
- (c) => c.connected && (c.capabilities?.[capability] || c.capabilities === void 0)
1167
- );
1168
- }
1169
- async listResources() {
1170
- const resources = [];
1171
- for (const conn of this.connectedWith("resources")) {
1761
+ session.lastSeen = Date.now();
1172
1762
  try {
1173
- const page = await conn.client.listResources(void 0, {
1174
- timeout: conn.upstream.timeout_ms
1175
- });
1176
- for (const r of page.resources) {
1177
- this.resourceOwner.set(r.uri, conn.upstream.name);
1178
- resources.push(r);
1179
- }
1180
- } catch {
1763
+ return withCors(await session.transport.handleRequest(request), cors);
1764
+ } catch (err) {
1765
+ return errorResponse(err, cors);
1181
1766
  }
1182
1767
  }
1183
- return { resources };
1184
- }
1185
- async listResourceTemplates() {
1186
- const resourceTemplates = [];
1187
- for (const conn of this.connectedWith("resources")) {
1188
- try {
1189
- const page = await conn.client.listResourceTemplates(void 0, {
1190
- timeout: conn.upstream.timeout_ms
1191
- });
1192
- for (const t of page.resourceTemplates) {
1193
- this.templateOwner.set(t.uriTemplate, conn.upstream.name);
1194
- resourceTemplates.push(t);
1195
- }
1196
- } catch {
1768
+ if (request.method !== "POST") return methodNotAllowed(request.method, "POST, OPTIONS", cors);
1769
+ sweep();
1770
+ const server = factory();
1771
+ const transport = new WebStandardStreamableHTTPServerTransport({
1772
+ ...transportBase,
1773
+ sessionIdGenerator: () => crypto.randomUUID(),
1774
+ onsessioninitialized: (id) => {
1775
+ sessions.set(id, { server, transport, lastSeen: Date.now() });
1776
+ },
1777
+ onsessionclosed: (id) => {
1778
+ sessions.delete(id);
1197
1779
  }
1780
+ });
1781
+ try {
1782
+ await server.connect(transport);
1783
+ const previousOnClose = transport.onclose;
1784
+ transport.onclose = () => {
1785
+ previousOnClose?.();
1786
+ if (transport.sessionId) sessions.delete(transport.sessionId);
1787
+ };
1788
+ const response = withCors(await transport.handleRequest(request), cors);
1789
+ if (!transport.sessionId) server.close().catch(() => void 0);
1790
+ return response;
1791
+ } catch (err) {
1792
+ server.close().catch(() => void 0);
1793
+ return errorResponse(err, cors);
1198
1794
  }
1199
- return { resourceTemplates };
1200
- }
1201
- ownerForUri(uri) {
1202
- const owner = this.resourceOwner.get(uri);
1203
- if (owner && this.connections.get(owner)?.connected) return [this.connections.get(owner)];
1204
- for (const [template, name] of this.templateOwner) {
1205
- if (templateMatches(template, uri) && this.connections.get(name)?.connected)
1206
- return [this.connections.get(name)];
1795
+ };
1796
+ }
1797
+ function createHttpHandler(opts, transportOptions = {}) {
1798
+ const { factory, transport: t } = splitOptions(opts, transportOptions);
1799
+ const cors = corsHeaders(t.cors);
1800
+ const sessions = t.sessions ? typeof t.sessions === "object" ? t.sessions : {} : void 0;
1801
+ const context = {
1802
+ factory,
1803
+ cors,
1804
+ transportBase: {
1805
+ enableJsonResponse: t.jsonResponse ?? sessions === void 0,
1806
+ allowedHosts: t.allowedHosts,
1807
+ allowedOrigins: t.allowedOrigins,
1808
+ enableDnsRebindingProtection: t.enableDnsRebindingProtection
1207
1809
  }
1208
- return this.connectedWith("resources");
1209
- }
1210
- async readResource(uri) {
1211
- let lastError;
1212
- for (const conn of this.ownerForUri(uri)) {
1213
- try {
1214
- const result = await conn.client.readResource(
1215
- { uri },
1216
- { timeout: conn.upstream.timeout_ms }
1810
+ };
1811
+ const handler = sessions ? createStatefulHandler(context, sessions) : createStatelessHandler(context);
1812
+ const allow = sessions ? "GET, POST, DELETE, OPTIONS" : "POST, OPTIONS";
1813
+ return async (request) => {
1814
+ if (request.method === "OPTIONS") {
1815
+ return new Response(null, { status: 204, headers: { Allow: allow, ...cors } });
1816
+ }
1817
+ return handler(request);
1818
+ };
1819
+ }
1820
+ function localhostHosts(port) {
1821
+ const hosts = ["localhost", "127.0.0.1", "[::1]"];
1822
+ return [...hosts, ...hosts.map((h) => `${h}:${port}`)];
1823
+ }
1824
+ function handleHealth(info) {
1825
+ const tools = typeof info.tools === "number" ? info.tools : info.tools.length;
1826
+ return jsonResponse({ ok: true, name: info.name, version: info.version, tools }, 200);
1827
+ }
1828
+ async function serveNodeHttp(opts) {
1829
+ const http = await import("http");
1830
+ const host = opts.host ?? "127.0.0.1";
1831
+ const path2 = opts.path ?? "/mcp";
1832
+ const server = http.createServer((req, res) => {
1833
+ void (async () => {
1834
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? host}`);
1835
+ let response;
1836
+ if (url.pathname === path2) {
1837
+ const controller = new AbortController();
1838
+ res.on("close", () => {
1839
+ if (!res.writableFinished) controller.abort();
1840
+ });
1841
+ const headers = new Headers();
1842
+ for (const [key, value] of Object.entries(req.headers)) {
1843
+ if (Array.isArray(value)) for (const item of value) headers.append(key, item);
1844
+ else if (typeof value === "string") headers.set(key, value);
1845
+ }
1846
+ const method = req.method ?? "GET";
1847
+ let body;
1848
+ if (method !== "GET" && method !== "HEAD") {
1849
+ const chunks = [];
1850
+ for await (const chunk of req) chunks.push(chunk);
1851
+ body = new Uint8Array(Buffer.concat(chunks));
1852
+ }
1853
+ response = await opts.handler(
1854
+ new Request(url, { method, headers, body, signal: controller.signal })
1855
+ );
1856
+ } else if (url.pathname === "/health" && opts.health) {
1857
+ response = opts.health();
1858
+ } else {
1859
+ response = new Response(
1860
+ JSON.stringify({
1861
+ code: "NOT_FOUND",
1862
+ cause: `no route for ${url.pathname}`,
1863
+ fix: `POST JSON-RPC to ${path2}`,
1864
+ retryable: false
1865
+ }),
1866
+ { status: 404, headers: { "Content-Type": "application/json" } }
1217
1867
  );
1218
- this.resourceOwner.set(uri, conn.upstream.name);
1219
- return result;
1220
- } catch (err) {
1221
- lastError = err;
1222
1868
  }
1223
- }
1224
- throw lastError ?? new Error(`no upstream serves resource ${uri}`);
1225
- }
1226
- async subscribe(uri, on) {
1227
- for (const conn of this.ownerForUri(uri)) {
1228
- try {
1229
- if (on) await conn.client.subscribeResource({ uri });
1230
- else await conn.client.unsubscribeResource({ uri });
1869
+ const outHeaders = {};
1870
+ response.headers.forEach((value, key) => {
1871
+ outHeaders[key] = value;
1872
+ });
1873
+ res.writeHead(response.status, outHeaders);
1874
+ if (!response.body) {
1875
+ res.end();
1231
1876
  return;
1232
- } catch {
1233
1877
  }
1234
- }
1235
- }
1236
- async listPrompts() {
1237
- const prompts = [];
1238
- const taken = /* @__PURE__ */ new Set();
1239
- this.promptOwner.clear();
1240
- for (const conn of this.connectedWith("prompts")) {
1878
+ const reader = response.body.getReader();
1241
1879
  try {
1242
- const page = await conn.client.listPrompts(void 0, {
1243
- timeout: conn.upstream.timeout_ms
1244
- });
1245
- for (const p of page.prompts) {
1246
- const name = taken.has(p.name) ? `${conn.upstream.name}__${p.name}` : p.name;
1247
- taken.add(name);
1248
- this.promptOwner.set(name, { upstream: conn.upstream.name, original: p.name });
1249
- prompts.push({ ...p, name });
1880
+ for (; ; ) {
1881
+ const { done, value } = await reader.read();
1882
+ if (done) break;
1883
+ res.write(value);
1250
1884
  }
1251
1885
  } catch {
1252
1886
  }
1887
+ res.end();
1888
+ })().catch((err) => {
1889
+ if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
1890
+ res.end(
1891
+ JSON.stringify({
1892
+ code: "INTERNAL",
1893
+ cause: err instanceof Error ? err.message : String(err),
1894
+ fix: "retry; if it persists, report the request id",
1895
+ retryable: false
1896
+ })
1897
+ );
1898
+ });
1899
+ });
1900
+ await new Promise((resolve5, reject) => {
1901
+ server.once("error", reject);
1902
+ server.listen(opts.port ?? 8765, host, () => resolve5());
1903
+ });
1904
+ const address = server.address();
1905
+ const port = typeof address === "object" && address ? address.port : opts.port ?? 8765;
1906
+ return {
1907
+ port,
1908
+ url: `http://${host}:${port}${path2}`,
1909
+ close: () => new Promise((resolve5, reject) => {
1910
+ server.closeAllConnections();
1911
+ server.close((err) => err ? reject(err) : resolve5());
1912
+ })
1913
+ };
1914
+ }
1915
+
1916
+ // ../../packages/mcp-kit/dist/chunk-M567BWMA.js
1917
+ var LIST_MAX_PAGES = 20;
1918
+ var LIST_MAX_ITEMS = 2e3;
1919
+ async function readAllPages(fetchPage, opts = {}) {
1920
+ const maxPages = opts.maxPages ?? LIST_MAX_PAGES;
1921
+ const maxItems = opts.maxItems ?? LIST_MAX_ITEMS;
1922
+ const method = opts.method ?? "tools/list";
1923
+ const items = [];
1924
+ const followed = /* @__PURE__ */ new Set();
1925
+ let cursor;
1926
+ for (let page = 1; ; page++) {
1927
+ const res = await fetchPage(cursor, page);
1928
+ if (Array.isArray(res.items)) items.push(...res.items);
1929
+ const next = res.nextCursor;
1930
+ const more = typeof next === "string" && next !== "";
1931
+ if (items.length > maxItems) {
1932
+ return {
1933
+ items,
1934
+ pages: page,
1935
+ truncated: {
1936
+ ceiling: "items",
1937
+ reason: `${method} listed more than ${maxItems} items in ${page} page${page === 1 ? "" : "s"}${more ? " and had more" : ""}; at most ${maxItems} are read`
1938
+ }
1939
+ };
1253
1940
  }
1254
- return { prompts };
1255
- }
1256
- async getPrompt(name, args) {
1257
- if (this.promptOwner.size === 0) await this.listPrompts();
1258
- const owner = this.promptOwner.get(name);
1259
- const conns = owner ? [this.connections.get(owner.upstream)] : this.connectedWith("prompts");
1260
- let lastError;
1261
- for (const conn of conns) {
1262
- try {
1263
- return await conn.client.getPrompt(
1264
- { name: owner?.original ?? name, arguments: args },
1265
- { timeout: conn.upstream.timeout_ms }
1266
- );
1267
- } catch (err) {
1268
- lastError = err;
1269
- }
1270
- }
1271
- throw lastError ?? new Error(`no upstream serves prompt ${name}`);
1272
- }
1273
- async complete(params) {
1274
- let conns;
1275
- if (params.ref.type === "ref/prompt") {
1276
- if (this.promptOwner.size === 0) await this.listPrompts();
1277
- const owner = this.promptOwner.get(params.ref.name);
1278
- conns = owner ? [this.connections.get(owner.upstream)] : this.connectedWith("completions");
1279
- if (owner) params = { ...params, ref: { ...params.ref, name: owner.original } };
1280
- } else {
1281
- if (this.templateOwner.size === 0) await this.listResourceTemplates();
1282
- conns = this.ownerForUri(params.ref.uri);
1283
- }
1284
- let lastError;
1285
- for (const conn of conns) {
1286
- try {
1287
- return await conn.client.complete(params, { timeout: conn.upstream.timeout_ms });
1288
- } catch (err) {
1289
- lastError = err;
1290
- }
1291
- }
1292
- throw lastError ?? new Error("no upstream supports completions");
1293
- }
1294
- async setLevel(level) {
1295
- await Promise.all(
1296
- this.connectedWith("logging").map(
1297
- (c) => c.client.setLoggingLevel(level).catch(() => void 0)
1298
- )
1299
- );
1300
- }
1301
- async close() {
1302
- this.closing = true;
1303
- for (const conn of this.connections.values()) {
1304
- try {
1305
- await conn.client.close();
1306
- } catch {
1307
- }
1941
+ if (!more) return { items, pages: page };
1942
+ if (followed.has(next)) return { items, pages: page, repeatedCursor: true };
1943
+ if (page >= maxPages) {
1944
+ return {
1945
+ items,
1946
+ pages: page,
1947
+ truncated: {
1948
+ ceiling: "pages",
1949
+ reason: `${method} had more than ${maxPages} pages (${items.length} items read); at most ${maxPages} pages are read`
1950
+ }
1951
+ };
1308
1952
  }
1309
- this.connections.clear();
1953
+ followed.add(next);
1954
+ cursor = next;
1310
1955
  }
1311
- };
1312
- function templateMatches(template, uri) {
1313
- const re = new RegExp(
1314
- "^" + template.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\{[^}]+\\\}/g, "[^/]+") + "$"
1315
- );
1316
- return re.test(uri);
1317
1956
  }
1318
- async function withTimeout(promise, ms, what) {
1319
- let timer;
1320
- const timeout = new Promise((_, reject) => {
1321
- timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms}ms`)), ms);
1957
+
1958
+ // ../../packages/mcp-kit/dist/index.js
1959
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
1960
+ async function serveStdio(serverOrOpts) {
1961
+ const server = serverOrOpts instanceof McpServer2 ? serverOrOpts : createMcpServer(serverOrOpts);
1962
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
1963
+ const transport = new StdioServerTransport();
1964
+ await server.connect(transport);
1965
+ await new Promise((resolve5) => {
1966
+ const previous = transport.onclose;
1967
+ transport.onclose = () => {
1968
+ previous?.();
1969
+ resolve5();
1970
+ };
1322
1971
  });
1323
- try {
1324
- return await Promise.race([promise, timeout]);
1325
- } finally {
1326
- if (timer) clearTimeout(timer);
1327
- }
1328
1972
  }
1329
1973
 
1330
- // src/proxy/server.ts
1331
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1332
- import {
1333
- CallToolRequestSchema,
1334
- CompleteRequestSchema,
1335
- GetPromptRequestSchema,
1336
- ListPromptsRequestSchema,
1337
- ListResourceTemplatesRequestSchema,
1338
- ListResourcesRequestSchema,
1339
- ListToolsRequestSchema,
1340
- ReadResourceRequestSchema,
1341
- SetLevelRequestSchema,
1342
- SubscribeRequestSchema,
1343
- UnsubscribeRequestSchema
1344
- } from "@modelcontextprotocol/sdk/types.js";
1345
- import {
1346
- keyFromHeaders,
1347
- resolveAgent
1348
- } from "@agentwares/agentguard-core";
1349
- var PROXY_VERSION = version();
1350
- function headerValue(extra, name) {
1351
- const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
1352
- if (Array.isArray(raw)) return raw[0];
1353
- return typeof raw === "string" ? raw : void 0;
1354
- }
1355
- function headersOf(extra) {
1356
- const out = {};
1357
- for (const [k, v] of Object.entries(extra.requestInfo?.headers ?? {}))
1358
- out[k.toLowerCase()] = Array.isArray(v) ? v[0] : v;
1359
- return out;
1360
- }
1361
- function runIdFor(extra, meta, fallback) {
1362
- const header = headerValue(extra, "x-run-id");
1363
- if (header && header.trim()) return header.trim().slice(0, 128);
1364
- const fromMeta = meta?.runId ?? meta?.["agentguard/runId"] ?? meta?.run_id;
1365
- if (typeof fromMeta === "string" && fromMeta.trim()) return fromMeta.trim().slice(0, 128);
1366
- if (extra.sessionId) return `sess_${extra.sessionId.slice(0, 12)}`;
1367
- return fallback;
1368
- }
1369
- function toCallToolResult(result, tool) {
1370
- const meta = {
1371
- agentguard: {
1372
- class: result.decision.classification.class,
1373
- verb: result.decision.classification.verb,
1374
- mode: result.decision.mode,
1375
- outcome: result.outcome,
1376
- dryRun: result.faked,
1377
- seq: result.entry.seq,
1378
- run_id: result.entry.run_id
1379
- }
1380
- };
1381
- const hasOutputSchema = Boolean(tool?.tool.outputSchema);
1382
- if (result.error) {
1383
- const upstream2 = result.value;
1384
- if (upstream2 && upstream2.content) {
1385
- return { ...upstream2, isError: true, _meta: { ...upstream2._meta ?? {}, ...meta } };
1974
+ // src/proxy/runtime.ts
1975
+ var ProxyRuntime = class {
1976
+ loaded;
1977
+ guard;
1978
+ agent;
1979
+ defaultRunId;
1980
+ configured;
1981
+ sessions = /* @__PURE__ */ new Set();
1982
+ connections = /* @__PURE__ */ new Map();
1983
+ exposed = [];
1984
+ resourceOwner = /* @__PURE__ */ new Map();
1985
+ templateOwner = /* @__PURE__ */ new Map();
1986
+ promptOwner = /* @__PURE__ */ new Map();
1987
+ log;
1988
+ connectTimeoutMs;
1989
+ testClients;
1990
+ /** stop relaying upstream stderr once we are tearing down — shutdown noise is not the user's problem */
1991
+ closing = false;
1992
+ constructor(opts) {
1993
+ this.loaded = opts.loaded;
1994
+ this.guard = opts.guard;
1995
+ this.agent = opts.agent;
1996
+ this.defaultRunId = opts.defaultRunId;
1997
+ this.configured = opts.configured ?? true;
1998
+ this.log = opts.log ?? ((line) => process.stderr.write(`[agentguard] ${line}
1999
+ `));
2000
+ this.connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
2001
+ this.testClients = opts.clients ?? {};
2002
+ }
2003
+ get policy() {
2004
+ return this.loaded.policy;
2005
+ }
2006
+ async start() {
2007
+ await Promise.all(this.policy.upstreams.map((u) => this.connect(u)));
2008
+ for (const [name, client] of Object.entries(this.testClients)) {
2009
+ if (this.connections.has(name)) continue;
2010
+ const upstream = {
2011
+ name,
2012
+ command: "in-memory",
2013
+ args: [],
2014
+ env: {},
2015
+ headers: {},
2016
+ prefix: false,
2017
+ timeout_ms: 6e4
2018
+ };
2019
+ const conn = {
2020
+ upstream,
2021
+ client,
2022
+ tools: [],
2023
+ capabilities: client.getServerCapabilities(),
2024
+ connected: true
2025
+ };
2026
+ this.connections.set(name, conn);
2027
+ this.wire(conn);
2028
+ await this.refreshTools(name);
1386
2029
  }
1387
- const body = result.error;
1388
- return {
1389
- isError: true,
1390
- content: [{ type: "text", text: JSON.stringify(body) }],
1391
- ...hasOutputSchema ? {} : { structuredContent: body },
1392
- _meta: { ...meta, httpStatus: httpStatusFor(body.code) }
2030
+ this.rebuildExposed();
2031
+ const status = this.upstreamStatus();
2032
+ this.log(
2033
+ `connected ${status.filter((s) => s.connected).length}/${status.length} upstreams, ${this.exposed.length} tools, mode ${this.policy.mode}`
2034
+ );
2035
+ }
2036
+ async connect(upstream) {
2037
+ const client = new Client(
2038
+ { name: "agentguard", version: version() },
2039
+ { capabilities: { sampling: {}, elicitation: {} } }
2040
+ );
2041
+ const conn = {
2042
+ upstream,
2043
+ client,
2044
+ tools: [],
2045
+ capabilities: void 0,
2046
+ connected: false
1393
2047
  };
2048
+ this.connections.set(upstream.name, conn);
2049
+ try {
2050
+ const transport = upstream.url ? new StreamableHTTPClientTransport(new URL(upstream.url), {
2051
+ requestInit: { headers: upstreamHeaders(upstream) }
2052
+ }) : new StdioClientTransport({
2053
+ command: upstream.command,
2054
+ args: upstream.args,
2055
+ env: { ...getDefaultEnvironment(), ...upstream.env },
2056
+ cwd: upstream.cwd,
2057
+ stderr: "pipe"
2058
+ });
2059
+ if (transport instanceof StdioClientTransport) {
2060
+ transport.stderr?.on("data", (chunk) => {
2061
+ if (this.closing) return;
2062
+ for (const line of chunk.toString("utf8").split("\n"))
2063
+ if (line.trim()) this.log(`${upstream.name}: ${line}`);
2064
+ });
2065
+ }
2066
+ await withTimeout(
2067
+ client.connect(transport),
2068
+ this.connectTimeoutMs,
2069
+ `connect to upstream "${upstream.name}"`
2070
+ );
2071
+ conn.capabilities = client.getServerCapabilities();
2072
+ conn.connected = true;
2073
+ this.wire(conn);
2074
+ await this.refreshTools(upstream.name);
2075
+ } catch (err) {
2076
+ conn.connected = false;
2077
+ conn.error = err instanceof Error ? err.message : String(err);
2078
+ this.log(`upstream "${upstream.name}" unavailable: ${conn.error}`);
2079
+ }
1394
2080
  }
1395
- if (result.faked) {
1396
- const value = result.value;
1397
- const isObject = value !== null && typeof value === "object" && !Array.isArray(value);
1398
- return {
1399
- content: [
1400
- { type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
1401
- ],
1402
- ...isObject ? { structuredContent: value } : {},
1403
- _meta: meta
2081
+ wire(conn) {
2082
+ const { client, upstream } = conn;
2083
+ client.onclose = () => {
2084
+ conn.connected = false;
2085
+ conn.error = "connection closed";
2086
+ if (!this.closing) this.log(`upstream "${upstream.name}" closed`);
1404
2087
  };
2088
+ client.setNotificationHandler(LoggingMessageNotificationSchema, async (n) => {
2089
+ await this.broadcast(upstream.name, n.method, n.params);
2090
+ });
2091
+ client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (n) => {
2092
+ await this.broadcast(upstream.name, n.method, n.params);
2093
+ });
2094
+ client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
2095
+ this.resourceOwner.clear();
2096
+ await this.broadcast(upstream.name, "notifications/resources/list_changed", void 0);
2097
+ });
2098
+ client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
2099
+ this.promptOwner.clear();
2100
+ await this.broadcast(upstream.name, "notifications/prompts/list_changed", void 0);
2101
+ });
2102
+ client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
2103
+ await this.refreshTools(upstream.name);
2104
+ this.rebuildExposed();
2105
+ for (const s of this.sessions) s.server.server.sendToolListChanged().catch(() => void 0);
2106
+ });
2107
+ try {
2108
+ client.setRequestHandler(CreateMessageRequestSchema, async (req) => {
2109
+ const session = this.pickSession(upstream.name);
2110
+ if (!session) throw new Error("no downstream client is connected to relay sampling to");
2111
+ const related = session.inflight.get(upstream.name)?.at(-1);
2112
+ return session.server.server.createMessage(req.params, { relatedRequestId: related });
2113
+ });
2114
+ client.setRequestHandler(ElicitRequestSchema, async (req) => {
2115
+ const session = this.pickSession(upstream.name);
2116
+ if (!session) throw new Error("no downstream client is connected to relay elicitation to");
2117
+ const related = session.inflight.get(upstream.name)?.at(-1);
2118
+ return session.server.server.elicitInput(req.params, { relatedRequestId: related });
2119
+ });
2120
+ } catch {
2121
+ }
1405
2122
  }
1406
- const upstream = result.value;
1407
- return { ...upstream, _meta: { ...upstream._meta ?? {}, ...meta } };
1408
- }
1409
- function httpStatusFor(code) {
1410
- switch (code) {
1411
- case "KILLED":
1412
- case "TOOL_DENIED":
1413
- case "APPROVAL_DENIED":
1414
- return 403;
1415
- case "APPROVAL_REQUIRED":
1416
- return 202;
1417
- case "CAP_EXCEEDED":
1418
- case "LOOP_DETECTED":
1419
- return 429;
1420
- case "UNKNOWN_TOOL":
1421
- return 404;
1422
- case "UPSTREAM_ERROR":
1423
- return 502;
1424
- default:
1425
- return 500;
2123
+ pickSession(upstream) {
2124
+ let best;
2125
+ for (const s of this.sessions) {
2126
+ if ((s.inflight.get(upstream)?.length ?? 0) > 0) return s;
2127
+ if (!best || s.lastActive > best.lastActive) best = s;
2128
+ }
2129
+ return best;
1426
2130
  }
1427
- }
1428
- function annotateTool(t, mode) {
1429
- return {
1430
- ...t.tool,
1431
- name: t.name,
1432
- _meta: {
1433
- ...t.tool._meta ?? {},
1434
- agentguard: {
1435
- upstream: t.upstream,
1436
- class: t.classification.class,
1437
- verb: t.classification.verb,
1438
- mode
2131
+ async broadcast(upstream, method, params) {
2132
+ for (const s of this.sessions) {
2133
+ const related = s.inflight.get(upstream)?.at(-1);
2134
+ try {
2135
+ await s.server.server.notification(
2136
+ { method, params },
2137
+ related !== void 0 ? { relatedRequestId: related } : void 0
2138
+ );
2139
+ } catch {
1439
2140
  }
1440
2141
  }
1441
- };
1442
- }
1443
- function killedError(state) {
1444
- return Object.assign(
1445
- new Error(`agentguard kill switch is on (${state.source ?? "?"}: ${state.reason ?? ""})`),
1446
- { code: -32e3 }
1447
- );
1448
- }
1449
- function createDownstreamServer(runtime, opts = {}) {
1450
- const server = new McpServer(
1451
- { name: "agentguard", version: PROXY_VERSION, title: "agentguard policy proxy" },
1452
- {
1453
- capabilities: {
1454
- tools: { listChanged: true },
1455
- resources: { subscribe: true, listChanged: true },
1456
- prompts: { listChanged: true },
1457
- logging: {},
1458
- completions: {}
1459
- },
1460
- instructions: `Tool calls pass through agentguard (${runtime.policy.mode} mode). Errors carry { code, cause, fix, retryable }; follow "fix". CAP_EXCEEDED, LOOP_DETECTED and KILLED are not retryable: stop and report to the user. APPROVAL_REQUIRED: relay the approval command to the user, then retry the identical call once.`
1461
- }
1462
- );
1463
- const session = {
1464
- id: crypto.randomUUID(),
1465
- server,
1466
- lastActive: Date.now(),
1467
- inflight: /* @__PURE__ */ new Map()
1468
- };
1469
- runtime.attach(session);
1470
- const previousOnClose = server.server.onclose;
1471
- server.server.onclose = () => {
1472
- previousOnClose?.();
1473
- runtime.detach(session);
1474
- };
1475
- const touch = () => {
1476
- session.lastActive = Date.now();
1477
- };
1478
- const agentFor = async (extra) => {
1479
- const key = keyFromHeaders(headersOf(extra));
1480
- if (!key) return { agent: void 0 };
1481
- const resolved = await resolveAgent(runtime.policy, { key });
1482
- if (resolved.error) {
1483
- return {
1484
- agent: null,
1485
- error: {
1486
- code: "TOOL_DENIED",
1487
- cause: "the agent key in Authorization / X-Agentguard-Key is not in agentguard.yaml",
1488
- fix: "use a key created with `agentguard key create <agent>`, or omit the key to use the global policy",
1489
- retryable: false
1490
- }
1491
- };
1492
- }
1493
- return { agent: resolved.agent };
1494
- };
1495
- const low = server.server;
1496
- low.setRequestHandler(ListToolsRequestSchema, async (_req, extra) => {
1497
- touch();
1498
- const { agent } = await agentFor(extra);
1499
- const scope = agent === void 0 ? runtime.agent : agent;
1500
- return {
1501
- tools: runtime.listTools(scope).map((t) => annotateTool(t, scope?.mode ?? runtime.policy.mode))
1502
- };
1503
- });
1504
- low.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
1505
- touch();
1506
- const { agent, error } = await agentFor(extra);
1507
- const runId = runIdFor(extra, req.params._meta, opts.defaultRunId ?? runtime.defaultRunId);
1508
- if (error) {
1509
- return {
1510
- isError: true,
1511
- content: [{ type: "text", text: JSON.stringify(error) }],
1512
- structuredContent: error,
1513
- _meta: { httpStatus: 401 }
1514
- };
2142
+ }
2143
+ /**
2144
+ * Re-read an upstream's tools, every page (mcp-kit's `readAllPages`): at most 20 requests, each
2145
+ * under the upstream's `timeout_ms`, ending at a cursor already followed. It used to follow
2146
+ * `nextCursor` with no bound, so an upstream that echoed its cursor kept the proxy asking for
2147
+ * the same page forever. A refresh that fails, or stops at the 20-page / 2,000-tool ceiling,
2148
+ * changes nothing: the agent keeps the last good list, and the log says why.
2149
+ */
2150
+ async refreshTools(name) {
2151
+ const conn = this.connections.get(name);
2152
+ if (!conn?.connected) return;
2153
+ if (conn.capabilities && !conn.capabilities.tools) {
2154
+ conn.tools = [];
2155
+ return;
1515
2156
  }
1516
- const progressToken = req.params._meta?.progressToken;
1517
- const { result, tool } = await runtime.callTool(req.params.name, req.params.arguments ?? {}, {
1518
- runId,
1519
- agent,
1520
- sessionId: extra.sessionId ?? session.id,
1521
- session,
1522
- requestId: extra.requestId,
1523
- onprogress: progressToken === void 0 ? void 0 : (p) => {
1524
- void extra.sendNotification({
1525
- method: "notifications/progress",
1526
- params: { progressToken, ...p }
2157
+ const kept = () => `keeping the last good list (${conn.tools.length} tool${conn.tools.length === 1 ? "" : "s"})`;
2158
+ try {
2159
+ const read = await readAllPages(async (cursor) => {
2160
+ const page = await conn.client.listTools(cursor ? { cursor } : void 0, {
2161
+ timeout: conn.upstream.timeout_ms
1527
2162
  });
2163
+ return { items: page.tools, nextCursor: page.nextCursor };
2164
+ });
2165
+ if (read.truncated) {
2166
+ this.log(`tools/list for "${name}" not applied: ${read.truncated.reason}; ${kept()}`);
2167
+ return;
1528
2168
  }
1529
- });
1530
- return toCallToolResult(result, tool);
1531
- });
1532
- const guardRead = async () => {
1533
- const state = await runtime.guard.killState();
1534
- if (state.killed) throw killedError(state);
1535
- };
1536
- low.setRequestHandler(ListResourcesRequestSchema, async () => {
1537
- touch();
1538
- return runtime.listResources();
1539
- });
1540
- low.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
1541
- touch();
1542
- return runtime.listResourceTemplates();
1543
- });
1544
- low.setRequestHandler(ReadResourceRequestSchema, async (req) => {
1545
- touch();
1546
- await guardRead();
1547
- return runtime.readResource(req.params.uri);
1548
- });
1549
- low.setRequestHandler(SubscribeRequestSchema, async (req) => {
1550
- await runtime.subscribe(req.params.uri, true);
1551
- return {};
1552
- });
1553
- low.setRequestHandler(UnsubscribeRequestSchema, async (req) => {
1554
- await runtime.subscribe(req.params.uri, false);
1555
- return {};
1556
- });
1557
- low.setRequestHandler(ListPromptsRequestSchema, async () => {
1558
- touch();
1559
- return runtime.listPrompts();
1560
- });
1561
- low.setRequestHandler(GetPromptRequestSchema, async (req) => {
1562
- touch();
1563
- await guardRead();
1564
- return runtime.getPrompt(req.params.name, req.params.arguments);
1565
- });
1566
- low.setRequestHandler(CompleteRequestSchema, async (req) => {
1567
- touch();
1568
- return runtime.complete(req.params);
1569
- });
1570
- low.setRequestHandler(SetLevelRequestSchema, async (req) => {
1571
- await runtime.setLevel(req.params.level);
1572
- return {};
1573
- });
1574
- return server;
1575
- }
1576
-
1577
- // ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
1578
- import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
1579
- import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1580
- import {
1581
- CreateMessageResultSchema,
1582
- ElicitResultSchema,
1583
- SubscribeRequestSchema as SubscribeRequestSchema2,
1584
- UnsubscribeRequestSchema as UnsubscribeRequestSchema2
1585
- } from "@modelcontextprotocol/sdk/types.js";
1586
- import { z } from "zod";
1587
-
1588
- // ../../packages/x402/dist/index.js
1589
- var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1590
- var USDC_DECIMALS = 6;
1591
- var USDC_EIP712_DOMAIN = { name: "USD Coin", version: "2" };
1592
- function toAtomicUnits(amountUsd, decimals = USDC_DECIMALS) {
1593
- const trimmed = amountUsd.trim();
1594
- if (!/^\d+(\.\d+)?$/.test(trimmed)) throw new Error(`not a decimal amount: ${amountUsd}`);
1595
- const [whole = "0", fraction = ""] = trimmed.split(".");
1596
- if (fraction.length > decimals) {
1597
- throw new Error(`${amountUsd} has more than ${decimals} decimal places`);
2169
+ conn.tools = read.items;
2170
+ } catch (err) {
2171
+ this.log(
2172
+ `tools/list failed for "${name}": ${err instanceof Error ? err.message : String(err)}; ${kept()}`
2173
+ );
2174
+ }
2175
+ }
2176
+ rebuildExposed() {
2177
+ const taken = /* @__PURE__ */ new Set();
2178
+ const out = [];
2179
+ const order = [...this.policy.upstreams.map((u) => u.name), ...Object.keys(this.testClients)];
2180
+ for (const name of order) {
2181
+ const conn = this.connections.get(name);
2182
+ if (!conn) continue;
2183
+ const prefix = conn.upstream.prefix === true ? name : typeof conn.upstream.prefix === "string" ? conn.upstream.prefix : void 0;
2184
+ for (const tool of conn.tools) {
2185
+ let exposedName = prefix ? `${prefix}__${tool.name}` : tool.name;
2186
+ if (taken.has(exposedName)) exposedName = `${name}__${tool.name}`;
2187
+ if (taken.has(exposedName)) {
2188
+ this.log(`skipping duplicate tool ${exposedName} from ${name}`);
2189
+ continue;
2190
+ }
2191
+ taken.add(exposedName);
2192
+ out.push({
2193
+ name: exposedName,
2194
+ upstream: name,
2195
+ original: tool.name,
2196
+ tool,
2197
+ classification: classifyTool(
2198
+ {
2199
+ name: exposedName,
2200
+ description: tool.description,
2201
+ annotations: tool.annotations,
2202
+ inputSchema: tool.inputSchema,
2203
+ outputSchema: tool.outputSchema
2204
+ },
2205
+ this.policy
2206
+ )
2207
+ });
2208
+ }
2209
+ }
2210
+ this.exposed = out;
1598
2211
  }
1599
- return `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1600
- }
1601
- var DEFAULT_X402_NETWORK = "eip155:8453";
1602
- var X402_HEADER = "X-Payment-Requirements";
1603
- var X402_REQUIRED_HEADER = "PAYMENT-REQUIRED";
1604
- function toAmount(priceUsd) {
1605
- if (!Number.isFinite(priceUsd) || priceUsd < 0)
1606
- throw new Error("priceUsd must be a non-negative number");
1607
- return priceUsd.toFixed(6).replace(/0+$/, "").replace(/\.$/, ".0");
1608
- }
1609
- function buildPaymentRequired(opts) {
1610
- const amountUsd = toAmount(opts.priceUsd);
1611
- const creditsPayTo = opts.creditsPayTo ?? opts.selfServe?.keysUrl;
1612
- const accepts = [];
1613
- if (opts.payTo) {
1614
- accepts.push({
1615
- rail: "x402",
1616
- scheme: "exact",
1617
- amountUsd,
1618
- amount: toAtomicUnits(amountUsd, opts.assetDecimals),
1619
- resource: opts.resource,
1620
- description: opts.description,
1621
- mimeType: "application/json",
1622
- network: opts.network ?? DEFAULT_X402_NETWORK,
1623
- asset: opts.asset ?? USDC_BASE,
1624
- extra: opts.assetDomain ?? USDC_EIP712_DOMAIN,
1625
- payTo: opts.payTo,
1626
- maxTimeoutSeconds: 300,
1627
- ...opts.bazaar ? { bazaar: opts.bazaar } : {}
1628
- });
2212
+ listTools(agent) {
2213
+ if (agent?.upstreams) return this.exposed.filter((t) => agent.upstreams.includes(t.upstream));
2214
+ return this.exposed;
1629
2215
  }
1630
- if (creditsPayTo) {
1631
- accepts.push({
1632
- rail: "prepaid_credits",
1633
- amountUsd,
1634
- resource: opts.resource,
1635
- description: opts.description,
1636
- currency: "usd",
1637
- payTo: creditsPayTo,
1638
- maxTimeoutSeconds: 300,
1639
- // funding a balance ends at a hosted Stripe Checkout page, which an agent cannot fill in
1640
- humanRequired: true
1641
- });
2216
+ findTool(name) {
2217
+ return this.exposed.find((t) => t.name === name);
1642
2218
  }
1643
- const live = opts.live === true;
1644
- const selfServe = opts.selfServe;
1645
- const x402Rail = accepts.find((a) => a.rail === "x402" && a.payTo);
1646
- const usdcFix = x402Rail ? `Pay in USDC with no account and no human: take the \`accepts\` entry with \`rail: "x402"\`, sign an EIP-3009 transferWithAuthorization for ${x402Rail.amountUsd} USDC to ${x402Rail.payTo} on ${x402Rail.network}, and retry with it base64 in the X-PAYMENT header. The facilitator submits the transfer, so you need USDC and no gas. ` : "";
1647
- const sampleFix = opts.sampleMode ? "Free first: retry with sample=true \u2014 no key, no payment. " : "";
1648
- const cardFix = selfServe ? `Or pay by card: POST ${selfServe.keysUrl} (no auth, no signup) returns a key and a Stripe Checkout URL. Funding that key needs a person at a browser; if you are running unattended, surface the URL to your operator rather than retrying, or use the USDC rail above. Once funded, send Authorization: Bearer <key>, plus Idempotency-Key so a repeated call is charged once.` : "";
1649
- const liveFix = usdcFix || cardFix ? `${sampleFix}${usdcFix}${cardFix}`.trim() : "Retry with a PAYMENT header satisfying one of `accepts`, or send an API key with credit balance.";
1650
- const body = {
1651
- code: "PAYMENT_REQUIRED",
1652
- cause: live ? `This call costs $${amountUsd}. No valid payment or credit was presented.` : `This call costs $${amountUsd}. Pay-per-call (x402 / Stripe MPP) is coming soon; use an API key with prepaid credits.`,
1653
- fix: live ? liveFix : `Create an API key${opts.signupUrl ? ` at ${opts.signupUrl}` : ""} and retry with Authorization: Bearer <key>. Try the free sample mode first.`,
1654
- retryable: true,
1655
- priceUsd: opts.priceUsd,
1656
- accepts,
1657
- comingSoon: !live,
1658
- alternatives: [
1659
- ...creditsPayTo ? ["api_key_credits"] : [],
1660
- ...opts.sampleMode ? ["sample_mode"] : []
1661
- ],
1662
- ...selfServe ? { selfServe } : {},
1663
- ...opts.bazaar ? { extensions: { bazaar: opts.bazaar } } : {}
1664
- };
1665
- const encoded = JSON.stringify({
1666
- x402Version: 2,
1667
- accepts,
1668
- resource: opts.resource,
1669
- ...opts.bazaar ? { extensions: { bazaar: opts.bazaar } } : {}
1670
- });
1671
- const headers = {
1672
- "Content-Type": "application/json",
1673
- "WWW-Authenticate": `Payment realm="agentwares", methods="x402 prepaid_credits", price="${amountUsd} USD"`,
1674
- [X402_HEADER]: encoded,
1675
- [X402_REQUIRED_HEADER]: btoa(encoded),
1676
- "Cache-Control": "no-store"
1677
- };
1678
- return { status: 402, headers, body };
1679
- }
1680
- var StubVerifier = class {
1681
- async verify(request, requirement) {
1682
- const hasHeader = request.headers.has("PAYMENT") || request.headers.has("X-PAYMENT");
1683
- return {
1684
- ok: false,
1685
- rail: requirement.rail,
1686
- reason: hasHeader ? "payments are not live yet (phase 2: wallet). Use an API key with credits." : "no payment header present"
1687
- };
2219
+ upstreamStatus() {
2220
+ return [...this.connections.values()].map((c) => ({
2221
+ name: c.upstream.name,
2222
+ connected: c.connected,
2223
+ tools: c.tools.length,
2224
+ transport: c.upstream.url ? "http" : "stdio",
2225
+ error: c.error
2226
+ }));
1688
2227
  }
1689
- };
1690
-
1691
- // ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
1692
- import { z as z2 } from "zod";
1693
- import { z as z3 } from "zod";
1694
- import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
1695
- import {
1696
- CallToolRequestSchema as CallToolRequestSchema2,
1697
- ErrorCode,
1698
- ListToolsRequestSchema as ListToolsRequestSchema2,
1699
- McpError
1700
- } from "@modelcontextprotocol/sdk/types.js";
1701
- import { z as z4 } from "zod";
1702
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1703
- var ERROR_CODES = [
1704
- "INVALID_INPUT",
1705
- "NOT_FOUND",
1706
- "UNAUTHORIZED",
1707
- "PAYMENT_REQUIRED",
1708
- "RATE_LIMITED",
1709
- "UPSTREAM_ERROR",
1710
- "INTERNAL"
1711
- ];
1712
- var DEFAULT_RETRYABLE = {
1713
- INVALID_INPUT: false,
1714
- NOT_FOUND: false,
1715
- UNAUTHORIZED: false,
1716
- PAYMENT_REQUIRED: true,
1717
- RATE_LIMITED: true,
1718
- UPSTREAM_ERROR: true,
1719
- INTERNAL: false
1720
- };
1721
- var DEFAULT_HTTP_STATUS = {
1722
- INVALID_INPUT: 400,
1723
- NOT_FOUND: 404,
1724
- UNAUTHORIZED: 401,
1725
- PAYMENT_REQUIRED: 402,
1726
- RATE_LIMITED: 429,
1727
- UPSTREAM_ERROR: 502,
1728
- INTERNAL: 500
1729
- };
1730
- var INTERNAL_FIX = "retry; if it persists, report the request id";
1731
- function isStandardCode(code) {
1732
- return ERROR_CODES.includes(code);
1733
- }
1734
- var McpToolError = class extends Error {
1735
- code;
1736
- cause;
1737
- fix;
1738
- retryable;
1739
- details;
1740
- httpStatus;
1741
- extra;
1742
- constructor(init) {
1743
- super(`${init.code}: ${init.cause}`);
1744
- this.name = "McpToolError";
1745
- this.code = init.code;
1746
- this.cause = init.cause;
1747
- this.fix = init.fix;
1748
- this.retryable = init.retryable ?? (isStandardCode(init.code) ? DEFAULT_RETRYABLE[init.code] : false);
1749
- this.details = init.details;
1750
- this.httpStatus = init.httpStatus ?? (isStandardCode(init.code) ? DEFAULT_HTTP_STATUS[init.code] : 500);
1751
- this.extra = init.extra;
2228
+ attach(session) {
2229
+ this.sessions.add(session);
1752
2230
  }
1753
- /** The body agents see: `{ code, cause, fix, retryable, details?, ...extra }`. */
1754
- toJSON() {
1755
- const body = {
1756
- ...this.extra,
1757
- code: this.code,
1758
- cause: this.cause,
1759
- fix: this.fix,
1760
- retryable: this.retryable
1761
- };
1762
- if (this.details !== void 0) body.details = this.details;
1763
- return body;
2231
+ detach(session) {
2232
+ this.sessions.delete(session);
1764
2233
  }
1765
- };
1766
- function toolError(codeOrInit, cause, fix, extra) {
1767
- if (typeof codeOrInit === "object") return new McpToolError(codeOrInit);
1768
- return new McpToolError({ code: codeOrInit, cause: cause ?? "", fix: fix ?? "", ...extra });
1769
- }
1770
- function describeUnknown(err) {
1771
- if (err instanceof Error) return err.message || err.name;
1772
- if (typeof err === "string") return err;
1773
- try {
1774
- return JSON.stringify(err);
1775
- } catch {
1776
- return String(err);
2234
+ /** Run a tool call through the policy engine and, if allowed, the upstream. */
2235
+ async callTool(name, args, ctx) {
2236
+ const tool = this.findTool(name);
2237
+ const agent = ctx.agent === void 0 ? this.agent : ctx.agent;
2238
+ if (!tool) {
2239
+ const result2 = await this.guard.run(
2240
+ { tool: { name }, args, runId: ctx.runId, agent, sessionId: ctx.sessionId },
2241
+ async () => {
2242
+ throw new GuardError({
2243
+ code: "UNKNOWN_TOOL",
2244
+ cause: `no upstream exposes a tool named "${name}"`,
2245
+ fix: "call tools/list and use one of the listed names",
2246
+ retryable: false
2247
+ });
2248
+ }
2249
+ );
2250
+ return { result: result2 };
2251
+ }
2252
+ const conn = this.connections.get(tool.upstream);
2253
+ const track = () => {
2254
+ if (!ctx.session || ctx.requestId === void 0) return () => void 0;
2255
+ const list = ctx.session.inflight.get(tool.upstream) ?? [];
2256
+ list.push(ctx.requestId);
2257
+ ctx.session.inflight.set(tool.upstream, list);
2258
+ return () => {
2259
+ const l = ctx.session.inflight.get(tool.upstream) ?? [];
2260
+ const i = l.indexOf(ctx.requestId);
2261
+ if (i >= 0) l.splice(i, 1);
2262
+ };
2263
+ };
2264
+ const result = await this.guard.run(
2265
+ {
2266
+ tool: {
2267
+ name: tool.name,
2268
+ description: tool.tool.description,
2269
+ annotations: tool.tool.annotations,
2270
+ inputSchema: tool.tool.inputSchema,
2271
+ outputSchema: tool.tool.outputSchema
2272
+ },
2273
+ args,
2274
+ runId: ctx.runId,
2275
+ agent,
2276
+ sessionId: ctx.sessionId,
2277
+ upstream: tool.upstream
2278
+ },
2279
+ async (finalArgs) => {
2280
+ if (!conn?.connected) {
2281
+ throw new GuardError({
2282
+ code: "UPSTREAM_ERROR",
2283
+ cause: `upstream "${tool.upstream}" is not connected${conn?.error ? ` (${conn.error})` : ""}`,
2284
+ fix: "check the upstream command/url in agentguard.yaml and restart the proxy",
2285
+ retryable: true
2286
+ });
2287
+ }
2288
+ const untrack = track();
2289
+ try {
2290
+ return await conn.client.callTool(
2291
+ { name: tool.original, arguments: finalArgs ?? {} },
2292
+ void 0,
2293
+ {
2294
+ timeout: conn.upstream.timeout_ms,
2295
+ resetTimeoutOnProgress: true,
2296
+ onprogress: ctx.onprogress
2297
+ }
2298
+ );
2299
+ } finally {
2300
+ untrack();
2301
+ }
2302
+ }
2303
+ );
2304
+ return { result, tool };
1777
2305
  }
1778
- }
1779
- function normalizeError(err) {
1780
- if (err instanceof McpToolError) return err;
1781
- return new McpToolError({
1782
- code: "INTERNAL",
1783
- cause: describeUnknown(err),
1784
- fix: INTERNAL_FIX,
1785
- retryable: false
1786
- });
1787
- }
1788
- function errorResult(err, opts = {}) {
1789
- const error = normalizeError(err);
1790
- const body = error.toJSON();
1791
- const meta = { httpStatus: error.httpStatus };
1792
- if (opts.requestId !== void 0) meta.requestId = opts.requestId;
1793
- Object.assign(meta, opts.meta);
1794
- return {
1795
- isError: true,
1796
- content: [{ type: "text", text: JSON.stringify(body) }],
1797
- structuredContent: body,
1798
- _meta: meta
1799
- };
1800
- }
1801
- var TOOL_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
1802
- var MIN_NAMESPACE_LENGTH = 3;
1803
- var MIN_DESCRIPTION_LENGTH = 20;
1804
- function isValidToolName(name) {
1805
- if (!TOOL_NAME_PATTERN.test(name)) return false;
1806
- const namespace = name.split("_")[0] ?? "";
1807
- return namespace.length >= MIN_NAMESPACE_LENGTH;
1808
- }
1809
- function assertToolName(name) {
1810
- if (!isValidToolName(name)) {
1811
- throw new Error(
1812
- `tool name must be namespaced: <namespace>_<verb>_<object>, got ${JSON.stringify(name)}`
2306
+ connectedWith(capability) {
2307
+ return [...this.connections.values()].filter(
2308
+ (c) => c.connected && (c.capabilities?.[capability] || c.capabilities === void 0)
1813
2309
  );
1814
2310
  }
1815
- }
1816
- function isPlainObject(value) {
1817
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1818
- const proto = Object.getPrototypeOf(value);
1819
- return proto === Object.prototype || proto === null;
1820
- }
1821
- function isCallToolResult(value) {
1822
- if (!isPlainObject(value) || !Array.isArray(value.content)) return false;
1823
- return value.content.every(
1824
- (block) => isPlainObject(block) && typeof block.type === "string"
1825
- );
1826
- }
1827
- function toCallToolResult2(value) {
1828
- if (isCallToolResult(value)) return value;
1829
- const text = typeof value === "string" ? value : JSON.stringify(value ?? null, null, 2);
1830
- const result = { content: [{ type: "text", text }] };
1831
- if (isPlainObject(value)) result.structuredContent = value;
1832
- return result;
1833
- }
1834
- function issuePath(issue) {
1835
- return issue.path.map(String).join(".") || "(root)";
1836
- }
1837
- function invalidInputError(issues) {
1838
- const fields = [...new Set(issues.map(issuePath))];
1839
- const summary = issues.map((issue) => `${issuePath(issue)}: ${issue.message}`).join("; ");
1840
- return toolError({
1841
- code: "INVALID_INPUT",
1842
- cause: `invalid input for ${fields.join(", ")} \u2014 ${summary}`,
1843
- fix: `fix ${fields.join(", ")} to match the tool's inputSchema (see details.issues), then retry`,
1844
- retryable: false,
1845
- details: {
1846
- fields,
1847
- issues: issues.map((issue) => ({
1848
- path: issue.path.map(String),
1849
- code: issue.code,
1850
- message: issue.message
1851
- }))
2311
+ async listResources() {
2312
+ const resources = [];
2313
+ for (const conn of this.connectedWith("resources")) {
2314
+ try {
2315
+ const page = await conn.client.listResources(void 0, {
2316
+ timeout: conn.upstream.timeout_ms
2317
+ });
2318
+ for (const r of page.resources) {
2319
+ this.resourceOwner.set(r.uri, conn.upstream.name);
2320
+ resources.push(r);
2321
+ }
2322
+ } catch {
2323
+ }
1852
2324
  }
1853
- });
1854
- }
1855
- function defineTool(opts) {
1856
- assertToolName(opts.name);
1857
- if (typeof opts.description !== "string" || opts.description.trim().length < MIN_DESCRIPTION_LENGTH) {
1858
- throw new Error(
1859
- `tool ${opts.name}: description must be at least ${MIN_DESCRIPTION_LENGTH} characters \u2014 say what it does, when to use it and what it returns`
1860
- );
2325
+ return { resources };
1861
2326
  }
1862
- const { name, title, description, input, output, annotations } = opts;
1863
- const handler = opts.handler.bind(opts);
1864
- const tool = {
1865
- name,
1866
- title,
1867
- description,
1868
- input,
1869
- output,
1870
- annotations,
1871
- handler,
1872
- async invoke(rawInput, ctx) {
1873
- const fail2 = (err) => {
1874
- const result = errorResult(err, { requestId: ctx.requestId });
1875
- if (output) delete result.structuredContent;
1876
- return result;
1877
- };
1878
- const parsed = input.safeParse(rawInput ?? {});
1879
- if (!parsed.success) return fail2(invalidInputError(parsed.error.issues));
2327
+ async listResourceTemplates() {
2328
+ const resourceTemplates = [];
2329
+ for (const conn of this.connectedWith("resources")) {
1880
2330
  try {
1881
- const value = await handler(parsed.data, ctx);
1882
- if (output && !isCallToolResult(value)) {
1883
- const checked = output.safeParse(value);
1884
- if (!checked.success) {
1885
- throw toolError({
1886
- code: "INTERNAL",
1887
- cause: `tool ${name} returned a value that does not match its output schema \u2014 ${checked.error.issues.map((issue) => `${issuePath(issue)}: ${issue.message}`).join("; ")}`,
1888
- fix: "this is a bug in the tool, not in your call; report it with the request id",
1889
- retryable: false,
1890
- details: { issues: checked.error.issues }
1891
- });
1892
- }
1893
- return toCallToolResult2(checked.data);
2331
+ const page = await conn.client.listResourceTemplates(void 0, {
2332
+ timeout: conn.upstream.timeout_ms
2333
+ });
2334
+ for (const t of page.resourceTemplates) {
2335
+ this.templateOwner.set(t.uriTemplate, conn.upstream.name);
2336
+ resourceTemplates.push(t);
1894
2337
  }
1895
- return toCallToolResult2(value);
1896
- } catch (err) {
1897
- return fail2(err);
2338
+ } catch {
1898
2339
  }
1899
2340
  }
1900
- };
1901
- return tool;
1902
- }
1903
- var PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
1904
- var WAV_BASE64 = "UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=";
1905
- var STATIC_TEXT_URI = "test://static-text";
1906
- var STATIC_TEXT = "This is a static text resource for testing.";
1907
- var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
1908
- function embeddedResource() {
1909
- return {
1910
- type: "resource",
1911
- resource: { uri: STATIC_TEXT_URI, mimeType: "text/plain", text: STATIC_TEXT }
1912
- };
1913
- }
1914
- function requireExtra(ctx) {
1915
- if (!ctx.extra) {
1916
- throw toolError("INTERNAL", "no client connection in this context", "call this tool over MCP");
2341
+ return { resourceTemplates };
1917
2342
  }
1918
- return ctx.extra;
1919
- }
1920
- async function elicit(ctx, message, requestedSchema) {
1921
- const result = await requireExtra(ctx).sendRequest(
1922
- { method: "elicitation/create", params: { message, requestedSchema } },
1923
- ElicitResultSchema
1924
- );
1925
- return `Elicitation completed: action=${result.action}, content=${JSON.stringify(result.content ?? {})}`;
1926
- }
1927
- var conformanceTools = [
1928
- defineTool({
1929
- name: "test_simple_text",
1930
- description: "Conformance fixture: returns a fixed simple text response.",
1931
- input: z.object({}),
1932
- handler: () => "This is a simple text response for testing."
1933
- }),
1934
- defineTool({
1935
- name: "test_error_handling",
1936
- description: "Conformance fixture: always returns an error result (isError: true).",
1937
- input: z.object({}),
1938
- handler: () => {
1939
- throw toolError({
1940
- code: "UPSTREAM_ERROR",
1941
- cause: "This tool intentionally returns an error for testing",
1942
- fix: "nothing to fix; this fixture always fails",
1943
- retryable: false
1944
- });
2343
+ ownerForUri(uri) {
2344
+ const owner = this.resourceOwner.get(uri);
2345
+ if (owner && this.connections.get(owner)?.connected) return [this.connections.get(owner)];
2346
+ for (const [template, name] of this.templateOwner) {
2347
+ if (templateMatches(template, uri) && this.connections.get(name)?.connected)
2348
+ return [this.connections.get(name)];
1945
2349
  }
1946
- }),
1947
- defineTool({
1948
- name: "test_image_content",
1949
- description: "Conformance fixture: returns a 1x1 PNG image content block.",
1950
- input: z.object({}),
1951
- handler: () => ({ content: [{ type: "image", data: PNG_BASE64, mimeType: "image/png" }] })
1952
- }),
1953
- defineTool({
1954
- name: "test_audio_content",
1955
- description: "Conformance fixture: returns an empty WAV audio content block.",
1956
- input: z.object({}),
1957
- handler: () => ({ content: [{ type: "audio", data: WAV_BASE64, mimeType: "audio/wav" }] })
1958
- }),
1959
- defineTool({
1960
- name: "test_embedded_resource",
1961
- description: "Conformance fixture: returns an embedded text resource content block.",
1962
- input: z.object({}),
1963
- handler: () => ({ content: [embeddedResource()] })
1964
- }),
1965
- defineTool({
1966
- name: "test_multiple_content_types",
1967
- description: "Conformance fixture: returns text, image and resource content blocks together.",
1968
- input: z.object({}),
1969
- handler: () => ({
1970
- content: [
1971
- { type: "text", text: "Mixed content response" },
1972
- { type: "image", data: PNG_BASE64, mimeType: "image/png" },
1973
- embeddedResource()
1974
- ]
1975
- })
1976
- }),
1977
- defineTool({
1978
- name: "test_tool_with_logging",
1979
- description: "Conformance fixture: emits three info log notifications while running.",
1980
- input: z.object({}),
1981
- handler: async (_input, ctx) => {
1982
- await ctx.log?.("info", "Tool execution started");
1983
- await sleep(50);
1984
- await ctx.log?.("info", "Tool processing data");
1985
- await sleep(50);
1986
- await ctx.log?.("info", "Tool execution completed");
1987
- return "Tool execution completed with logging";
2350
+ return this.connectedWith("resources");
2351
+ }
2352
+ async readResource(uri) {
2353
+ let lastError;
2354
+ for (const conn of this.ownerForUri(uri)) {
2355
+ try {
2356
+ const result = await conn.client.readResource(
2357
+ { uri },
2358
+ { timeout: conn.upstream.timeout_ms }
2359
+ );
2360
+ this.resourceOwner.set(uri, conn.upstream.name);
2361
+ return result;
2362
+ } catch (err) {
2363
+ lastError = err;
2364
+ }
1988
2365
  }
1989
- }),
1990
- defineTool({
1991
- name: "test_tool_with_progress",
1992
- description: "Conformance fixture: reports progress 0/100, 50/100, 100/100 while running.",
1993
- input: z.object({}),
1994
- handler: async (_input, ctx) => {
1995
- await ctx.progress?.(0, 100, "starting");
1996
- await sleep(50);
1997
- await ctx.progress?.(50, 100, "halfway");
1998
- await sleep(50);
1999
- await ctx.progress?.(100, 100, "done");
2000
- return "Tool execution completed with progress";
2366
+ throw lastError ?? new Error(`no upstream serves resource ${uri}`);
2367
+ }
2368
+ async subscribe(uri, on) {
2369
+ for (const conn of this.ownerForUri(uri)) {
2370
+ try {
2371
+ if (on) await conn.client.subscribeResource({ uri });
2372
+ else await conn.client.unsubscribeResource({ uri });
2373
+ return;
2374
+ } catch {
2375
+ }
2001
2376
  }
2002
- }),
2003
- defineTool({
2004
- name: "test_sampling",
2005
- description: "Conformance fixture: asks the client to sample a completion and returns it.",
2006
- input: z.object({ prompt: z.string().describe("Prompt to send to the client's model") }),
2007
- handler: async ({ prompt }, ctx) => {
2008
- const result = await requireExtra(ctx).sendRequest(
2009
- {
2010
- method: "sampling/createMessage",
2011
- params: {
2012
- messages: [{ role: "user", content: { type: "text", text: prompt } }],
2013
- maxTokens: 100
2014
- }
2015
- },
2016
- CreateMessageResultSchema
2017
- );
2018
- const content = Array.isArray(result.content) ? result.content[0] : result.content;
2019
- const text = content && content.type === "text" ? content.text : JSON.stringify(content);
2020
- return `Sampling completed (${result.model}): ${text}`;
2377
+ }
2378
+ async listPrompts() {
2379
+ const prompts = [];
2380
+ const taken = /* @__PURE__ */ new Set();
2381
+ this.promptOwner.clear();
2382
+ for (const conn of this.connectedWith("prompts")) {
2383
+ try {
2384
+ const page = await conn.client.listPrompts(void 0, {
2385
+ timeout: conn.upstream.timeout_ms
2386
+ });
2387
+ for (const p of page.prompts) {
2388
+ const name = taken.has(p.name) ? `${conn.upstream.name}__${p.name}` : p.name;
2389
+ taken.add(name);
2390
+ this.promptOwner.set(name, { upstream: conn.upstream.name, original: p.name });
2391
+ prompts.push({ ...p, name });
2392
+ }
2393
+ } catch {
2394
+ }
2021
2395
  }
2022
- }),
2023
- defineTool({
2024
- name: "test_elicitation",
2025
- description: "Conformance fixture: asks the client for user input and reports the outcome.",
2026
- input: z.object({ message: z.string().describe("Message shown to the user") }),
2027
- handler: ({ message }, ctx) => elicit(ctx, message, {
2028
- type: "object",
2029
- properties: {
2030
- name: { type: "string", title: "Name", description: "Your name" },
2031
- email: { type: "string", title: "Email", format: "email" }
2032
- },
2033
- required: ["name"]
2034
- })
2035
- }),
2036
- defineTool({
2037
- name: "test_elicitation_sep1034_defaults",
2038
- description: "Conformance fixture: elicitation schema with defaults for every primitive type.",
2039
- input: z.object({}),
2040
- handler: (_input, ctx) => elicit(ctx, "Please confirm your details", {
2041
- type: "object",
2042
- properties: {
2043
- name: { type: "string", title: "Name", default: "John Doe" },
2044
- age: { type: "integer", title: "Age", default: 30 },
2045
- score: { type: "number", title: "Score", default: 95.5 },
2046
- status: {
2047
- type: "string",
2048
- title: "Status",
2049
- enum: ["active", "inactive", "pending"],
2050
- default: "active"
2051
- },
2052
- verified: { type: "boolean", title: "Verified", default: true }
2396
+ return { prompts };
2397
+ }
2398
+ async getPrompt(name, args) {
2399
+ if (this.promptOwner.size === 0) await this.listPrompts();
2400
+ const owner = this.promptOwner.get(name);
2401
+ const conns = owner ? [this.connections.get(owner.upstream)] : this.connectedWith("prompts");
2402
+ let lastError;
2403
+ for (const conn of conns) {
2404
+ try {
2405
+ return await conn.client.getPrompt(
2406
+ { name: owner?.original ?? name, arguments: args },
2407
+ { timeout: conn.upstream.timeout_ms }
2408
+ );
2409
+ } catch (err) {
2410
+ lastError = err;
2053
2411
  }
2054
- })
2055
- }),
2056
- defineTool({
2057
- name: "test_elicitation_sep1330_enums",
2058
- description: "Conformance fixture: elicitation schema with titled, untitled and multi-select enums.",
2059
- input: z.object({}),
2060
- handler: (_input, ctx) => elicit(ctx, "Pick some options", {
2061
- type: "object",
2062
- properties: {
2063
- untitledSingle: {
2064
- type: "string",
2065
- title: "Untitled single",
2066
- enum: ["red", "green", "blue"]
2067
- },
2068
- titledSingle: {
2069
- type: "string",
2070
- title: "Titled single",
2071
- oneOf: [
2072
- { const: "red", title: "Red" },
2073
- { const: "green", title: "Green" },
2074
- { const: "blue", title: "Blue" }
2075
- ]
2076
- },
2077
- legacyEnum: {
2078
- type: "string",
2079
- title: "Legacy titled",
2080
- enum: ["red", "green", "blue"],
2081
- enumNames: ["Red", "Green", "Blue"]
2082
- },
2083
- untitledMulti: {
2084
- type: "array",
2085
- title: "Untitled multi",
2086
- items: { type: "string", enum: ["red", "green", "blue"] }
2087
- },
2088
- titledMulti: {
2089
- type: "array",
2090
- title: "Titled multi",
2091
- items: {
2092
- anyOf: [
2093
- { const: "red", title: "Red" },
2094
- { const: "green", title: "Green" },
2095
- { const: "blue", title: "Blue" }
2096
- ]
2097
- }
2098
- }
2412
+ }
2413
+ throw lastError ?? new Error(`no upstream serves prompt ${name}`);
2414
+ }
2415
+ async complete(params) {
2416
+ let conns;
2417
+ if (params.ref.type === "ref/prompt") {
2418
+ if (this.promptOwner.size === 0) await this.listPrompts();
2419
+ const owner = this.promptOwner.get(params.ref.name);
2420
+ conns = owner ? [this.connections.get(owner.upstream)] : this.connectedWith("completions");
2421
+ if (owner) params = { ...params, ref: { ...params.ref, name: owner.original } };
2422
+ } else {
2423
+ if (this.templateOwner.size === 0) await this.listResourceTemplates();
2424
+ conns = this.ownerForUri(params.ref.uri);
2425
+ }
2426
+ let lastError;
2427
+ for (const conn of conns) {
2428
+ try {
2429
+ return await conn.client.complete(params, { timeout: conn.upstream.timeout_ms });
2430
+ } catch (err) {
2431
+ lastError = err;
2099
2432
  }
2100
- })
2101
- }),
2102
- defineTool({
2103
- name: "json_schema_2020_12_tool",
2104
- description: "Conformance fixture: input schema with a $defs reference to an address object.",
2105
- input: z.object({
2106
- name: z.string(),
2107
- address: z.object({ street: z.string(), city: z.string() }).meta({ id: "address" })
2108
- }),
2109
- handler: (input) => input
2110
- })
2111
- ];
2112
- function paymentRequiredError(body, reason) {
2113
- const { code, cause, fix, retryable, ...rest } = body;
2433
+ }
2434
+ throw lastError ?? new Error("no upstream supports completions");
2435
+ }
2436
+ async setLevel(level) {
2437
+ await Promise.all(
2438
+ this.connectedWith("logging").map(
2439
+ (c) => c.client.setLoggingLevel(level).catch(() => void 0)
2440
+ )
2441
+ );
2442
+ }
2443
+ async close() {
2444
+ this.closing = true;
2445
+ for (const conn of this.connections.values()) {
2446
+ try {
2447
+ await conn.client.close();
2448
+ } catch {
2449
+ }
2450
+ }
2451
+ this.connections.clear();
2452
+ }
2453
+ };
2454
+ function templateMatches(template, uri) {
2455
+ const re = new RegExp(
2456
+ "^" + template.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\{[^}]+\\\}/g, "[^/]+") + "$"
2457
+ );
2458
+ return re.test(uri);
2459
+ }
2460
+ async function withTimeout(promise, ms, what) {
2461
+ let timer;
2462
+ const timeout = new Promise((_, reject) => {
2463
+ timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms}ms`)), ms);
2464
+ });
2465
+ try {
2466
+ return await Promise.race([promise, timeout]);
2467
+ } finally {
2468
+ if (timer) clearTimeout(timer);
2469
+ }
2470
+ }
2471
+
2472
+ // src/proxy/server.ts
2473
+ import { McpServer as McpServer3 } from "@modelcontextprotocol/sdk/server/mcp.js";
2474
+ import {
2475
+ CallToolRequestSchema as CallToolRequestSchema2,
2476
+ CompleteRequestSchema,
2477
+ GetPromptRequestSchema,
2478
+ ListPromptsRequestSchema,
2479
+ ListResourceTemplatesRequestSchema,
2480
+ ListResourcesRequestSchema,
2481
+ ListToolsRequestSchema as ListToolsRequestSchema2,
2482
+ ReadResourceRequestSchema,
2483
+ SetLevelRequestSchema,
2484
+ SubscribeRequestSchema as SubscribeRequestSchema2,
2485
+ UnsubscribeRequestSchema as UnsubscribeRequestSchema2
2486
+ } from "@modelcontextprotocol/sdk/types.js";
2487
+ import {
2488
+ keyFromHeaders,
2489
+ resolveAgent
2490
+ } from "@agentwares/agentguard-core";
2491
+
2492
+ // src/proxy/own-tools.ts
2493
+ import {
2494
+ buildReport,
2495
+ latestRunId,
2496
+ listRuns,
2497
+ parseAuditJsonl,
2498
+ renderReportMarkdown,
2499
+ verifyChain
2500
+ } from "@agentwares/agentguard-core";
2501
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
2502
+ import { z as z5 } from "zod";
2503
+ var INIT_COMMAND = "npx -y @agentwares/agentguard init";
2504
+ var READ_ONLY = {
2505
+ readOnlyHint: true,
2506
+ destructiveHint: false,
2507
+ idempotentHint: true,
2508
+ openWorldHint: false
2509
+ };
2510
+ var runIdInput = z5.string().min(1).max(128).optional().describe(
2511
+ "Run to read, as it appears in the audit log (for example run_20261007093015_ab12, or the X-Run-Id an HTTP client sent). Omit it for the most recent run."
2512
+ );
2513
+ var counters = z5.record(z5.string(), z5.number()).describe(
2514
+ "counter name (tool_calls, writes, deletes, emails, spend_usd or a custom one) \u2192 value"
2515
+ );
2516
+ function readAudit(path2) {
2517
+ if (!existsSync(path2)) return { exists: false, entries: [] };
2518
+ try {
2519
+ return { exists: true, entries: parseAuditJsonl(readFileSync2(path2, "utf8")) };
2520
+ } catch (err) {
2521
+ return { exists: true, entries: [], error: err instanceof Error ? err.message : String(err) };
2522
+ }
2523
+ }
2524
+ function unreadable(path2, error) {
2114
2525
  return toolError({
2115
- code,
2116
- cause,
2117
- fix,
2118
- retryable,
2119
- httpStatus: 402,
2120
- details: reason !== void 0 ? { reason } : void 0,
2121
- extra: rest
2526
+ code: "AUDIT_UNREADABLE",
2527
+ cause: `the audit log at ${path2} could not be read: ${error}`,
2528
+ fix: "call agentguard_verify_audit_log to see where it breaks; a hand-edited or truncated line has to be restored from a copy",
2529
+ retryable: false
2122
2530
  });
2123
2531
  }
2124
- function formatUsd(priceUsd) {
2125
- return priceUsd.toFixed(6).replace(/0+$/, "").replace(/\.$/, ".00").replace(/\.(\d)$/, ".$10");
2532
+ function numbers(caps) {
2533
+ const out = {};
2534
+ for (const [k, v] of Object.entries(caps)) if (typeof v === "number") out[k] = v;
2535
+ return out;
2126
2536
  }
2127
- function withPayment(tool, opts) {
2128
- if (!Number.isFinite(opts.priceUsd) || opts.priceUsd < 0) {
2129
- throw new Error(`withPayment(${tool.name}): priceUsd must be a non-negative number`);
2130
- }
2131
- const sampleField = opts.sampleField ?? "sample";
2132
- if (sampleField in tool.input.shape) {
2133
- throw new Error(
2134
- `withPayment(${tool.name}): input already has a field named ${JSON.stringify(sampleField)}; pass sampleField to rename it`
2135
- );
2136
- }
2137
- const verifier = opts.verifier ?? new StubVerifier();
2138
- const liveRail = opts.verifier !== void 0;
2139
- const input = tool.input.extend({
2140
- [sampleField]: z2.boolean().default(false).describe(
2141
- "Set true to return an example response at no charge (sample mode). Default false."
2142
- )
2537
+ function nextStep(runtime) {
2538
+ if (!runtime.configured)
2539
+ return `agentguard is guarding nothing: no policy file at ${runtime.loaded.path}. Run \`${INIT_COMMAND}\` in the project directory: it writes agentguard.yaml in dry-run mode and routes the project's MCP servers through agentguard (backup kept, \`--undo\` restores). Then restart the MCP client.`;
2540
+ if (runtime.policy.upstreams.length === 0)
2541
+ return `${runtime.loaded.path} lists no upstream servers, so agentguard is guarding nothing. Add the MCP servers your agent uses under \`upstreams:\` in that file (it has commented examples), then restart the MCP client.`;
2542
+ return "agentguard is running; run your agent, then read agentguard_get_report.";
2543
+ }
2544
+ var StatusOutput = z5.object({
2545
+ configured: z5.boolean().describe(
2546
+ "true when a policy file was found and loaded; false means agentguard guards nothing"
2547
+ ),
2548
+ policy_path: z5.string().describe("absolute path of the policy file read, or looked for"),
2549
+ mode: z5.enum(["dry-run", "enforce"]).describe("dry-run fakes writes and spends and records them; enforce lets allowed calls run"),
2550
+ upstreams: z5.array(
2551
+ z5.object({
2552
+ name: z5.string().describe("upstream name from the policy"),
2553
+ connected: z5.boolean().describe("whether agentguard is connected to it"),
2554
+ tools: z5.number().int().describe("tools it exposes"),
2555
+ transport: z5.enum(["stdio", "http"]).describe("how agentguard reaches it"),
2556
+ error: z5.string().optional().describe("why it is not connected")
2557
+ })
2558
+ ).describe("the MCP servers this proxy fronts; empty means none are configured"),
2559
+ run_id: z5.string().nullable().describe(
2560
+ "run the usage is for: the requested run_id, else the latest run in the log, else null"
2561
+ ),
2562
+ caps: z5.object({ per_run: counters, per_day: counters }).describe("limits from the policy; a counter that is absent has no cap"),
2563
+ used: z5.object({ per_run: counters, per_day: counters }).describe("what that run, and today, have used against the caps"),
2564
+ remaining: z5.record(
2565
+ z5.string(),
2566
+ z5.object({ per_run: z5.number().optional(), per_day: z5.number().optional() })
2567
+ ).describe("budget left per capped counter, per run and per day"),
2568
+ kill_switch: z5.object({
2569
+ on: z5.boolean().describe("true halts every tool call with KILLED"),
2570
+ reason: z5.string().optional().describe("why it was pulled"),
2571
+ source: z5.string().optional().describe("file, env or memory")
2572
+ }).describe("the kill switch state"),
2573
+ pending_approvals: z5.number().int().describe("destructive calls waiting for a human decision"),
2574
+ audit_log: z5.object({
2575
+ path: z5.string().describe("absolute path of the hash-chained JSONL audit log"),
2576
+ entries: z5.number().int().describe("entries in it; 0 when it does not exist yet")
2577
+ }).describe("where every tool call is recorded"),
2578
+ next_step: z5.string().describe("the one thing to do next, with the command when there is one")
2579
+ });
2580
+ var ReportOutput = z5.object({
2581
+ configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
2582
+ audit_log_path: z5.string().describe("absolute path of the audit log the report was built from"),
2583
+ run_id: z5.string().nullable().describe("the run reported on; null when the log has no runs"),
2584
+ runs_in_log: z5.number().int().describe("how many runs the audit log holds"),
2585
+ tool_calls: z5.number().int().describe("tool calls in this run"),
2586
+ would_have: z5.object({
2587
+ creates: z5.number().int(),
2588
+ updates: z5.number().int(),
2589
+ deletes: z5.number().int(),
2590
+ sends: z5.number().int(),
2591
+ executes: z5.number().int(),
2592
+ spends: z5.number().int(),
2593
+ spend_usd: z5.number()
2594
+ }).describe("what dry-run mode faked: what the run would have done had the calls executed"),
2595
+ did: z5.object({
2596
+ writes: z5.number().int(),
2597
+ deletes: z5.number().int(),
2598
+ sends: z5.number().int(),
2599
+ spend_usd: z5.number()
2600
+ }).describe("what actually executed upstream"),
2601
+ halts: z5.array(
2602
+ z5.object({
2603
+ seq: z5.number().int().describe("audit entry number"),
2604
+ ts: z5.string().describe("ISO time of the call"),
2605
+ code: z5.string().describe("CAP_EXCEEDED, LOOP_DETECTED, KILLED, APPROVAL_REQUIRED, \u2026"),
2606
+ tool: z5.string().describe("the tool that was stopped"),
2607
+ cause: z5.string().describe("why")
2608
+ })
2609
+ ).describe("every call agentguard stopped, in order"),
2610
+ top_tools: z5.array(z5.object({ tool: z5.string(), calls: z5.number().int(), class: z5.string() })).describe("the most-called tools with their class (read, write, spend)"),
2611
+ markdown: z5.string().describe("the same report `agentguard report` prints, as Markdown")
2612
+ });
2613
+ var VerifyOutput = z5.object({
2614
+ configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
2615
+ path: z5.string().describe("absolute path of the audit log checked"),
2616
+ exists: z5.boolean().describe("false when no call has been recorded yet; an absent log verifies"),
2617
+ ok: z5.boolean().describe("true when every entry's hash and prev_hash recompute"),
2618
+ entries: z5.number().int().describe("entries checked"),
2619
+ head_hash: z5.string().nullable().describe(
2620
+ "hash of the last entry; record it with the count to detect entries cut from the end"
2621
+ ),
2622
+ broken_at: z5.number().int().nullable().describe("seq of the first entry that does not verify"),
2623
+ reason: z5.string().nullable().describe("why verification failed")
2624
+ });
2625
+ function createOwnTools(runtime) {
2626
+ const status = defineTool({
2627
+ name: "agentguard_get_status",
2628
+ title: "agentguard status",
2629
+ description: "Report what agentguard is guarding right now: the policy file it loaded (or that none was found), dry-run or enforce mode, each upstream MCP server and whether it connected, a run's cap usage and remaining budget, the kill switch, and pending approvals. Call it first to learn whether agentguard is set up; next_step says what to do when it is not. For what a run did, use agentguard_get_report; to check the log was not edited, agentguard_verify_audit_log. Reads local files only and changes nothing.",
2630
+ input: z5.strictObject({ run_id: runIdInput }),
2631
+ output: StatusOutput,
2632
+ annotations: { title: "agentguard status", ...READ_ONLY },
2633
+ async handler({ run_id }) {
2634
+ const { loaded, guard } = runtime;
2635
+ const audit = readAudit(loaded.auditPath);
2636
+ if (audit.error) throw unreadable(loaded.auditPath, audit.error);
2637
+ const runId = run_id ?? latestRunId(audit.entries) ?? null;
2638
+ const state = await guard.status(runId ?? "(none yet)");
2639
+ const pending = await guard.approvals.list("pending");
2640
+ return {
2641
+ configured: runtime.configured,
2642
+ policy_path: loaded.path,
2643
+ mode: runtime.policy.mode,
2644
+ upstreams: runtime.upstreamStatus(),
2645
+ run_id: runId,
2646
+ caps: { per_run: numbers(state.caps.per_run), per_day: numbers(state.caps.per_day) },
2647
+ used: state.usage,
2648
+ remaining: state.remaining,
2649
+ kill_switch: {
2650
+ on: state.killed.killed,
2651
+ ...state.killed.reason ? { reason: state.killed.reason } : {},
2652
+ ...state.killed.source ? { source: state.killed.source } : {}
2653
+ },
2654
+ pending_approvals: pending.length,
2655
+ audit_log: { path: loaded.auditPath, entries: audit.entries.length },
2656
+ next_step: nextStep(runtime)
2657
+ };
2658
+ }
2143
2659
  });
2144
- const description = `${tool.description.trim()} Paid: $${formatUsd(opts.priceUsd)} per call; without credit you get a PAYMENT_REQUIRED result. Set ${sampleField}=true for a free example response.`;
2145
- return defineTool({
2146
- name: tool.name,
2147
- title: tool.title,
2148
- description,
2149
- input,
2150
- output: tool.output,
2151
- annotations: tool.annotations,
2152
- async handler(raw, ctx) {
2153
- const record = { ...raw };
2154
- const sample = record[sampleField] === true;
2155
- delete record[sampleField];
2156
- if (sample) return tool.handler(record, { ...ctx, sample: true });
2157
- let reason;
2158
- if (opts.authorize) {
2159
- const auth = await opts.authorize(ctx);
2160
- if (auth.ok) return tool.handler(record, { ...ctx, userId: auth.userId });
2161
- reason = auth.reason;
2660
+ const report = defineTool({
2661
+ name: "agentguard_get_report",
2662
+ title: "agentguard run report",
2663
+ description: "Summarize one run from agentguard's audit log: how many tool calls, what dry-run mode faked (records it would have created, updated or deleted, messages sent, dollars spent), what really ran upstream, and every call agentguard halted with its code and cause. Defaults to the most recent run. Use it after an agent run, before switching a policy from dry-run to enforce; for live caps and the kill switch use agentguard_get_status. Read-only: reads the audit file and does not record this call.",
2664
+ input: z5.strictObject({ run_id: runIdInput }),
2665
+ output: ReportOutput,
2666
+ annotations: { title: "agentguard run report", ...READ_ONLY },
2667
+ handler({ run_id }) {
2668
+ const { loaded } = runtime;
2669
+ const audit = readAudit(loaded.auditPath);
2670
+ if (audit.error) throw unreadable(loaded.auditPath, audit.error);
2671
+ const runs = listRuns(audit.entries);
2672
+ if (run_id && !runs.some((r) => r.runId === run_id)) {
2673
+ throw toolError({
2674
+ code: "NOT_FOUND",
2675
+ cause: `no run "${run_id}" in the audit log at ${loaded.auditPath}`,
2676
+ fix: `call again without run_id for the latest run${runs.length ? `; runs in the log: ${runs.slice(0, 5).map((r) => r.runId).join(", ")}` : "; the log has no runs yet"}`,
2677
+ retryable: false
2678
+ });
2162
2679
  }
2163
- const resource = ctx.resourceUrl ?? tool.name;
2164
- const required = buildPaymentRequired({
2165
- resource,
2166
- priceUsd: opts.priceUsd,
2167
- live: opts.live,
2168
- description: opts.description,
2169
- payTo: opts.payTo,
2170
- creditsPayTo: opts.creditsPayTo,
2171
- signupUrl: opts.signupUrl,
2172
- selfServe: opts.selfServe,
2173
- // withPayment always adds a free `sample` flag to the tool's input schema
2174
- sampleMode: true,
2175
- ...opts.bazaar ? { bazaar: opts.bazaar } : {}
2176
- });
2177
- const advertised = required.body.accepts.find((a) => a.rail === "prepaid_credits") ?? required.body.accepts[0];
2178
- const requirement = advertised ?? {
2179
- rail: "prepaid_credits",
2180
- amountUsd: String(required.body.priceUsd),
2181
- resource,
2182
- ...opts.description ? { description: opts.description } : {}
2680
+ const runId = run_id ?? latestRunId(audit.entries) ?? null;
2681
+ const built = buildReport(audit.entries, { runId: runId ?? void 0 });
2682
+ const markdown = runId ? renderReportMarkdown(built) : `# agentguard report
2683
+
2684
+ No runs recorded in ${loaded.auditPath} yet. ${nextStep(runtime)}`;
2685
+ return {
2686
+ configured: runtime.configured,
2687
+ audit_log_path: loaded.auditPath,
2688
+ run_id: runId,
2689
+ runs_in_log: runs.length,
2690
+ tool_calls: runId ? audit.entries.filter((e) => e.run_id === runId).length : 0,
2691
+ would_have: {
2692
+ creates: built.wouldHave.creates,
2693
+ updates: built.wouldHave.updates,
2694
+ deletes: built.wouldHave.deletes,
2695
+ sends: built.wouldHave.sends,
2696
+ executes: built.wouldHave.executes,
2697
+ spends: built.wouldHave.spends,
2698
+ spend_usd: built.wouldHave.spendUsd
2699
+ },
2700
+ did: {
2701
+ writes: built.did.writes,
2702
+ deletes: built.did.deletes,
2703
+ sends: built.did.sends,
2704
+ spend_usd: built.did.spendUsd
2705
+ },
2706
+ halts: built.halts,
2707
+ top_tools: built.topTools,
2708
+ markdown
2183
2709
  };
2184
- const verified = await verifier.verify(
2185
- new Request(`https://mcp.local/${tool.name}`, { headers: ctx.headers }),
2186
- requirement
2187
- );
2188
- if (verified.ok) {
2189
- let result;
2190
- try {
2191
- result = await tool.handler(record, ctx);
2192
- } catch (error) {
2193
- await opts.onPaidFailure?.(ctx, verified, error);
2194
- throw error;
2195
- }
2196
- const settled = await verifier.settle?.(verified);
2197
- if (settled && !settled.ok) {
2198
- opts.onSettlementFailure?.(ctx, verified, settled.reason);
2199
- }
2200
- return result;
2710
+ }
2711
+ });
2712
+ const verify = defineTool({
2713
+ name: "agentguard_verify_audit_log",
2714
+ title: "Verify agentguard audit log",
2715
+ description: "Recompute the hash chain of agentguard's audit log (JSONL, one entry per tool call) and say whether any entry was edited, removed from the middle or reordered, naming the first broken entry. Returns the entry count and head hash; record both elsewhere, because entries cut from the end of the file still verify. Use it before trusting agentguard_get_report. Takes no arguments: it checks the log the loaded policy names. Read-only; a log that does not exist yet verifies as empty.",
2716
+ input: z5.strictObject({}),
2717
+ output: VerifyOutput,
2718
+ annotations: { title: "Verify agentguard audit log", ...READ_ONLY },
2719
+ async handler() {
2720
+ const { loaded } = runtime;
2721
+ const audit = readAudit(loaded.auditPath);
2722
+ if (audit.error) {
2723
+ return {
2724
+ configured: runtime.configured,
2725
+ path: loaded.auditPath,
2726
+ exists: true,
2727
+ ok: false,
2728
+ entries: 0,
2729
+ head_hash: null,
2730
+ broken_at: null,
2731
+ reason: audit.error
2732
+ };
2201
2733
  }
2202
- reason = liveRail ? verified.reason ?? reason : reason ?? verified.reason;
2203
- throw paymentRequiredError(required.body, reason);
2734
+ const result = await verifyChain(audit.entries);
2735
+ return {
2736
+ configured: runtime.configured,
2737
+ path: loaded.auditPath,
2738
+ exists: audit.exists,
2739
+ ok: result.ok,
2740
+ entries: result.entries,
2741
+ // an empty chain's head is the genesis hash, which is not an entry anyone can record
2742
+ head_hash: result.entries > 0 ? result.head ?? null : null,
2743
+ broken_at: result.brokenAt ?? null,
2744
+ reason: result.reason ?? null
2745
+ };
2204
2746
  }
2205
2747
  });
2748
+ return [status, report, verify];
2206
2749
  }
2207
- var exampleEcho = defineTool({
2208
- name: "example_echo",
2209
- title: "Echo",
2210
- description: "Echo a message back unchanged. Use it to check the connection and to see the result shape mcp-kit tools return (JSON text plus structuredContent).",
2211
- input: z3.object({ message: z3.string().describe("Text to echo back") }),
2212
- output: z3.object({ message: z3.string() }),
2213
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
2214
- handler: ({ message }) => ({ message })
2215
- });
2216
- var exampleAdd = defineTool({
2217
- name: "example_add",
2218
- title: "Add",
2219
- description: "Add two numbers and return their sum as { sum }. Demonstrates typed input and output schemas; send non-numbers to see an INVALID_INPUT error result.",
2220
- input: z3.object({
2221
- a: z3.number().describe("First addend"),
2222
- b: z3.number().describe("Second addend")
2223
- }),
2224
- output: z3.object({ sum: z3.number() }),
2225
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
2226
- handler: ({ a, b }) => ({ sum: a + b })
2227
- });
2228
- var examplePaidLookup = withPayment(
2229
- defineTool({
2230
- name: "example_paid_lookup",
2231
- title: "Paid lookup",
2232
- description: "Look up a record by query and return { query, result, sample } (fake data). Demonstrates a paid tool: without an entitlement the call returns a PAYMENT_REQUIRED result with x402 and MPP payment options.",
2233
- input: z3.object({ query: z3.string().min(1).describe("What to look up") }),
2234
- output: z3.object({ query: z3.string(), result: z3.string(), sample: z3.boolean() }),
2235
- annotations: { readOnlyHint: true, idempotentHint: true },
2236
- handler: ({ query }, ctx) => ({
2237
- query,
2238
- result: ctx.sample ? `SAMPLE: example record for "${query}"` : `record for "${query}"`,
2239
- sample: ctx.sample === true
2240
- })
2241
- }),
2242
- { priceUsd: 0.05, description: "One record lookup" }
2243
- );
2244
- function isZodObject(schema) {
2245
- return schema instanceof z4.ZodObject;
2750
+ function ownToolsActive(runtime) {
2751
+ return runtime.upstreamStatus().length === 0;
2246
2752
  }
2247
- function contextFromExtra(extra) {
2248
- const headers = new Headers();
2249
- const raw = extra.requestInfo?.headers;
2250
- if (raw) {
2251
- for (const [key, value] of Object.entries(raw)) {
2252
- if (Array.isArray(value)) for (const item of value) headers.append(key, item);
2253
- else if (typeof value === "string") headers.append(key, value);
2254
- }
2255
- }
2256
- const progressToken = extra._meta?.progressToken;
2257
- return {
2258
- headers,
2259
- requestId: String(extra.requestId),
2260
- signal: extra.signal,
2261
- sessionId: extra.sessionId,
2262
- meta: extra._meta,
2263
- extra,
2264
- log: async (level, data) => {
2265
- await extra.sendNotification({ method: "notifications/message", params: { level, data } });
2266
- },
2267
- progress: async (progress, total, message) => {
2268
- if (progressToken === void 0) return;
2269
- await extra.sendNotification({
2270
- method: "notifications/progress",
2271
- params: { progressToken, progress, total, message }
2272
- });
2273
- }
2274
- };
2753
+ function ownToolList(tools) {
2754
+ return listToolManifest(tools);
2275
2755
  }
2276
- function createMcpServer(opts) {
2277
- const byName = /* @__PURE__ */ new Map();
2278
- for (const tool of opts.tools) {
2279
- if (byName.has(tool.name)) throw new Error(`duplicate tool name: ${tool.name}`);
2280
- byName.set(tool.name, tool);
2281
- }
2282
- const server = new McpServer2(
2283
- { name: opts.name, version: opts.version, title: opts.title, websiteUrl: opts.websiteUrl },
2284
- { instructions: opts.instructions, capabilities: { logging: {}, ...opts.capabilities } }
2285
- );
2286
- const registered = /* @__PURE__ */ new Map();
2287
- for (const tool of byName.values()) {
2288
- const config = {
2289
- title: tool.title,
2290
- description: tool.description,
2291
- inputSchema: tool.input,
2292
- annotations: tool.annotations
2293
- };
2294
- if (isZodObject(tool.output)) config.outputSchema = tool.output;
2295
- registered.set(
2296
- tool.name,
2297
- server.registerTool(tool.name, config, () => {
2298
- throw new Error("unreachable: mcp-kit dispatches tools/call itself");
2299
- })
2300
- );
2301
- }
2302
- if (byName.size > 0) {
2303
- server.server.setRequestHandler(ListToolsRequestSchema2, async () => {
2304
- const enabled = [...byName.values()].filter(
2305
- (tool) => registered.get(tool.name)?.enabled !== false
2306
- );
2307
- return {
2308
- tools: listToolManifest(enabled).map((entry) => ({
2309
- ...entry,
2310
- inputSchema: entry.inputSchema,
2311
- outputSchema: entry.outputSchema
2312
- }))
2313
- };
2314
- });
2315
- server.server.setRequestHandler(CallToolRequestSchema2, async (request, extra) => {
2316
- const name = request.params.name;
2317
- const tool = byName.get(name);
2318
- const entry = registered.get(name);
2319
- if (!tool || !entry) throw new McpError(ErrorCode.InvalidParams, `Tool ${name} not found`);
2320
- if (!entry.enabled) throw new McpError(ErrorCode.InvalidParams, `Tool ${name} disabled`);
2321
- return tool.invoke(request.params.arguments ?? {}, contextFromExtra(extra));
2322
- });
2323
- }
2324
- return server;
2756
+ async function callOwnTool(tools, name, args, requestId) {
2757
+ const tool = tools.find((t) => t.name === name);
2758
+ if (!tool) return void 0;
2759
+ return tool.invoke(args, createToolContext({ requestId }));
2325
2760
  }
2326
- function toJsonSchema(schema, io = "input") {
2327
- const json2 = z4.toJSONSchema(schema, { io, target: "draft-2020-12" });
2328
- if (isZodObject(schema) && schema._zod.def.catchall === void 0 && json2.additionalProperties === void 0) {
2329
- json2.additionalProperties = false;
2330
- }
2331
- return json2;
2761
+
2762
+ // src/proxy/server.ts
2763
+ var PROXY_VERSION = version();
2764
+ function headerValue(extra, name) {
2765
+ const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
2766
+ if (Array.isArray(raw)) return raw[0];
2767
+ return typeof raw === "string" ? raw : void 0;
2332
2768
  }
2333
- function listToolManifest(tools) {
2334
- return tools.map((tool) => {
2335
- const entry = {
2336
- name: tool.name,
2337
- description: tool.description,
2338
- inputSchema: toJsonSchema(tool.input, "input")
2339
- };
2340
- if (tool.title !== void 0) entry.title = tool.title;
2341
- if (isZodObject(tool.output)) entry.outputSchema = toJsonSchema(tool.output, "output");
2342
- if (tool.annotations !== void 0) entry.annotations = tool.annotations;
2343
- return entry;
2344
- });
2769
+ function headersOf(extra) {
2770
+ const out = {};
2771
+ for (const [k, v] of Object.entries(extra.requestInfo?.headers ?? {}))
2772
+ out[k.toLowerCase()] = Array.isArray(v) ? v[0] : v;
2773
+ return out;
2774
+ }
2775
+ function runIdFor(extra, meta, fallback) {
2776
+ const header = headerValue(extra, "x-run-id");
2777
+ if (header && header.trim()) return header.trim().slice(0, 128);
2778
+ const fromMeta = meta?.runId ?? meta?.["agentguard/runId"] ?? meta?.run_id;
2779
+ if (typeof fromMeta === "string" && fromMeta.trim()) return fromMeta.trim().slice(0, 128);
2780
+ if (extra.sessionId) return `sess_${extra.sessionId.slice(0, 12)}`;
2781
+ return fallback;
2345
2782
  }
2346
- var DEFAULT_CORS = {
2347
- origin: "*",
2348
- methods: "GET, POST, DELETE, OPTIONS",
2349
- headers: "Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, PAYMENT, X-PAYMENT",
2350
- exposeHeaders: "Mcp-Session-Id, Mcp-Protocol-Version, WWW-Authenticate, X-Payment-Requirements, PAYMENT-REQUIRED",
2351
- maxAge: 86400
2352
- };
2353
- var DEFAULT_SESSION_TTL_MS = 30 * 6e4;
2354
- function corsHeaders(cors) {
2355
- if (!cors) return {};
2356
- const c = { ...DEFAULT_CORS, ...typeof cors === "object" ? cors : {} };
2357
- return {
2358
- "Access-Control-Allow-Origin": c.origin,
2359
- "Access-Control-Allow-Methods": c.methods,
2360
- "Access-Control-Allow-Headers": c.headers,
2361
- "Access-Control-Expose-Headers": c.exposeHeaders,
2362
- "Access-Control-Max-Age": String(c.maxAge)
2783
+ function toCallToolResult2(result, tool) {
2784
+ const meta = {
2785
+ agentguard: {
2786
+ class: result.decision.classification.class,
2787
+ verb: result.decision.classification.verb,
2788
+ mode: result.decision.mode,
2789
+ outcome: result.outcome,
2790
+ dryRun: result.faked,
2791
+ seq: result.entry.seq,
2792
+ run_id: result.entry.run_id
2793
+ }
2363
2794
  };
2795
+ const hasOutputSchema = Boolean(tool?.tool.outputSchema);
2796
+ if (result.error) {
2797
+ const upstream2 = result.value;
2798
+ if (upstream2 && upstream2.content) {
2799
+ return { ...upstream2, isError: true, _meta: { ...upstream2._meta ?? {}, ...meta } };
2800
+ }
2801
+ const body = result.error;
2802
+ return {
2803
+ isError: true,
2804
+ content: [{ type: "text", text: JSON.stringify(body) }],
2805
+ ...hasOutputSchema ? {} : { structuredContent: body },
2806
+ _meta: { ...meta, httpStatus: httpStatusFor(body.code) }
2807
+ };
2808
+ }
2809
+ if (result.faked) {
2810
+ const value = result.value;
2811
+ const isObject = value !== null && typeof value === "object" && !Array.isArray(value);
2812
+ return {
2813
+ content: [
2814
+ { type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
2815
+ ],
2816
+ ...isObject ? { structuredContent: value } : {},
2817
+ _meta: meta
2818
+ };
2819
+ }
2820
+ const upstream = result.value;
2821
+ return { ...upstream, _meta: { ...upstream._meta ?? {}, ...meta } };
2364
2822
  }
2365
- function jsonResponse(body, status, headers = {}) {
2366
- return new Response(JSON.stringify(body), {
2367
- status,
2368
- headers: { "Content-Type": "application/json", "Cache-Control": "no-store", ...headers }
2369
- });
2370
- }
2371
- function methodNotAllowed(method, allow, cors) {
2372
- return jsonResponse(
2373
- {
2374
- code: "METHOD_NOT_ALLOWED",
2375
- cause: `${method} is not supported here without a session`,
2376
- fix: "send JSON-RPC over POST with `Accept: application/json, text/event-stream` (start with `initialize`)",
2377
- retryable: false
2378
- },
2379
- 405,
2380
- { Allow: allow, ...cors }
2381
- );
2823
+ function httpStatusFor(code) {
2824
+ switch (code) {
2825
+ case "KILLED":
2826
+ case "TOOL_DENIED":
2827
+ case "APPROVAL_DENIED":
2828
+ return 403;
2829
+ case "APPROVAL_REQUIRED":
2830
+ return 202;
2831
+ case "CAP_EXCEEDED":
2832
+ case "LOOP_DETECTED":
2833
+ return 429;
2834
+ case "UNKNOWN_TOOL":
2835
+ return 404;
2836
+ case "UPSTREAM_ERROR":
2837
+ return 502;
2838
+ default:
2839
+ return 500;
2840
+ }
2382
2841
  }
2383
- function splitOptions(opts, transport) {
2384
- if (typeof opts === "function") return { factory: opts, transport };
2385
- const {
2386
- cors,
2387
- jsonResponse: jsonResponse2,
2388
- sessions,
2389
- allowedHosts,
2390
- allowedOrigins,
2391
- enableDnsRebindingProtection,
2392
- ...server
2393
- } = opts;
2842
+ function annotateTool(t, mode) {
2394
2843
  return {
2395
- factory: () => createMcpServer(server),
2396
- transport: {
2397
- cors,
2398
- jsonResponse: jsonResponse2,
2399
- sessions,
2400
- allowedHosts,
2401
- allowedOrigins,
2402
- enableDnsRebindingProtection,
2403
- ...transport
2844
+ ...t.tool,
2845
+ name: t.name,
2846
+ _meta: {
2847
+ ...t.tool._meta ?? {},
2848
+ agentguard: {
2849
+ upstream: t.upstream,
2850
+ class: t.classification.class,
2851
+ verb: t.classification.verb,
2852
+ mode
2853
+ }
2404
2854
  }
2405
2855
  };
2406
2856
  }
2407
- function withCors(response, cors) {
2408
- const keys = Object.keys(cors);
2409
- if (keys.length === 0) return response;
2410
- const headers = new Headers(response.headers);
2411
- for (const key of keys) headers.set(key, cors[key] ?? "");
2412
- return new Response(response.body, {
2413
- status: response.status,
2414
- statusText: response.statusText,
2415
- headers
2416
- });
2857
+ function killedError(state) {
2858
+ return Object.assign(
2859
+ new Error(`agentguard kill switch is on (${state.source ?? "?"}: ${state.reason ?? ""})`),
2860
+ { code: -32e3 }
2861
+ );
2417
2862
  }
2418
- function errorResponse(err, cors) {
2419
- const error = normalizeError(err);
2420
- return jsonResponse(error.toJSON(), error.httpStatus, cors);
2863
+ function serverInstructions(runtime) {
2864
+ if (ownToolsActive(runtime))
2865
+ return `agentguard is an MCP policy proxy, and this one fronts no upstream MCP servers${runtime.configured ? "" : ` (no policy file at ${runtime.loaded.path})`}, so it serves only its own read-only tools: agentguard_get_status (what it guards, caps, kill switch), agentguard_get_report (what a run did or would have done) and agentguard_verify_audit_log. To guard the user's MCP servers, run \`${INIT_COMMAND}\` in the project directory and restart the client.`;
2866
+ return `Tool calls pass through agentguard (${runtime.policy.mode} mode). Errors carry { code, cause, fix, retryable }; follow "fix". CAP_EXCEEDED, LOOP_DETECTED and KILLED are not retryable: stop and report to the user. APPROVAL_REQUIRED: relay the approval command to the user, then retry the identical call once.`;
2421
2867
  }
2422
- function createStatelessHandler({ factory, cors, transportBase }) {
2423
- return async (request) => {
2424
- if (request.method !== "POST") return methodNotAllowed(request.method, "POST, OPTIONS", cors);
2425
- const server = factory();
2426
- const transport = new WebStandardStreamableHTTPServerTransport({
2427
- ...transportBase,
2428
- sessionIdGenerator: void 0
2429
- });
2430
- let closed = false;
2431
- const cleanup = async () => {
2432
- if (closed) return;
2433
- closed = true;
2434
- try {
2435
- await server.close();
2436
- } catch {
2437
- }
2438
- };
2439
- try {
2440
- await server.connect(transport);
2441
- const response = withCors(await transport.handleRequest(request), cors);
2442
- if (!response.body) {
2443
- await cleanup();
2444
- return response;
2445
- }
2446
- request.signal?.addEventListener("abort", () => void cleanup(), { once: true });
2447
- const body = response.body.pipeThrough(
2448
- new TransformStream({ flush: () => cleanup() })
2449
- );
2450
- return new Response(body, {
2451
- status: response.status,
2452
- statusText: response.statusText,
2453
- headers: response.headers
2454
- });
2455
- } catch (err) {
2456
- await cleanup();
2457
- return errorResponse(err, cors);
2868
+ function createDownstreamServer(runtime, opts = {}) {
2869
+ const server = new McpServer3(
2870
+ { name: "agentguard", version: PROXY_VERSION, title: "agentguard policy proxy" },
2871
+ {
2872
+ capabilities: {
2873
+ tools: { listChanged: true },
2874
+ resources: { subscribe: true, listChanged: true },
2875
+ prompts: { listChanged: true },
2876
+ logging: {},
2877
+ completions: {}
2878
+ },
2879
+ instructions: serverInstructions(runtime)
2458
2880
  }
2881
+ );
2882
+ const ownTools = createOwnTools(runtime);
2883
+ const session = {
2884
+ id: crypto.randomUUID(),
2885
+ server,
2886
+ lastActive: Date.now(),
2887
+ inflight: /* @__PURE__ */ new Map()
2459
2888
  };
2460
- }
2461
- function createStatefulHandler({ factory, cors, transportBase }, options) {
2462
- const ttlMs = options.ttlMs ?? DEFAULT_SESSION_TTL_MS;
2463
- const sessions = /* @__PURE__ */ new Map();
2464
- const drop = (id) => {
2465
- const session = sessions.get(id);
2466
- if (!session) return;
2467
- sessions.delete(id);
2468
- session.server.close().catch(() => void 0);
2889
+ runtime.attach(session);
2890
+ const previousOnClose = server.server.onclose;
2891
+ server.server.onclose = () => {
2892
+ previousOnClose?.();
2893
+ runtime.detach(session);
2469
2894
  };
2470
- const sweep = () => {
2471
- const now = Date.now();
2472
- for (const [id, session] of sessions) if (now - session.lastSeen > ttlMs) drop(id);
2895
+ const touch = () => {
2896
+ session.lastActive = Date.now();
2473
2897
  };
2474
- return async (request) => {
2475
- const sessionId = request.headers.get("mcp-session-id");
2476
- if (sessionId) {
2477
- const session = sessions.get(sessionId);
2478
- if (!session) {
2479
- return jsonResponse(
2480
- {
2481
- code: "NOT_FOUND",
2482
- cause: "unknown or expired Mcp-Session-Id",
2483
- fix: "send `initialize` again without an Mcp-Session-Id header and use the id from the response",
2484
- retryable: true
2485
- },
2486
- 404,
2487
- cors
2488
- );
2489
- }
2490
- session.lastSeen = Date.now();
2491
- try {
2492
- return withCors(await session.transport.handleRequest(request), cors);
2493
- } catch (err) {
2494
- return errorResponse(err, cors);
2495
- }
2496
- }
2497
- if (request.method !== "POST") return methodNotAllowed(request.method, "POST, OPTIONS", cors);
2498
- sweep();
2499
- const server = factory();
2500
- const transport = new WebStandardStreamableHTTPServerTransport({
2501
- ...transportBase,
2502
- sessionIdGenerator: () => crypto.randomUUID(),
2503
- onsessioninitialized: (id) => {
2504
- sessions.set(id, { server, transport, lastSeen: Date.now() });
2505
- },
2506
- onsessionclosed: (id) => {
2507
- sessions.delete(id);
2508
- }
2509
- });
2510
- try {
2511
- await server.connect(transport);
2512
- const previousOnClose = transport.onclose;
2513
- transport.onclose = () => {
2514
- previousOnClose?.();
2515
- if (transport.sessionId) sessions.delete(transport.sessionId);
2898
+ const agentFor = async (extra) => {
2899
+ const key = keyFromHeaders(headersOf(extra));
2900
+ if (!key) return { agent: void 0 };
2901
+ const resolved = await resolveAgent(runtime.policy, { key });
2902
+ if (resolved.error) {
2903
+ return {
2904
+ agent: null,
2905
+ error: {
2906
+ code: "TOOL_DENIED",
2907
+ cause: "the agent key in Authorization / X-Agentguard-Key is not in agentguard.yaml",
2908
+ fix: "use a key created with `agentguard key create <agent>`, or omit the key to use the global policy",
2909
+ retryable: false
2910
+ }
2516
2911
  };
2517
- const response = withCors(await transport.handleRequest(request), cors);
2518
- if (!transport.sessionId) server.close().catch(() => void 0);
2519
- return response;
2520
- } catch (err) {
2521
- server.close().catch(() => void 0);
2522
- return errorResponse(err, cors);
2523
2912
  }
2913
+ return { agent: resolved.agent };
2524
2914
  };
2525
- }
2526
- function createHttpHandler(opts, transportOptions = {}) {
2527
- const { factory, transport: t } = splitOptions(opts, transportOptions);
2528
- const cors = corsHeaders(t.cors);
2529
- const sessions = t.sessions ? typeof t.sessions === "object" ? t.sessions : {} : void 0;
2530
- const context = {
2531
- factory,
2532
- cors,
2533
- transportBase: {
2534
- enableJsonResponse: t.jsonResponse ?? sessions === void 0,
2535
- allowedHosts: t.allowedHosts,
2536
- allowedOrigins: t.allowedOrigins,
2537
- enableDnsRebindingProtection: t.enableDnsRebindingProtection
2915
+ const low = server.server;
2916
+ low.setRequestHandler(ListToolsRequestSchema2, async (_req, extra) => {
2917
+ touch();
2918
+ const { agent } = await agentFor(extra);
2919
+ const scope = agent === void 0 ? runtime.agent : agent;
2920
+ if (ownToolsActive(runtime)) return { tools: ownToolList(ownTools) };
2921
+ return {
2922
+ tools: runtime.listTools(scope).map((t) => annotateTool(t, scope?.mode ?? runtime.policy.mode))
2923
+ };
2924
+ });
2925
+ low.setRequestHandler(CallToolRequestSchema2, async (req, extra) => {
2926
+ touch();
2927
+ if (ownToolsActive(runtime)) {
2928
+ const own = await callOwnTool(
2929
+ ownTools,
2930
+ req.params.name,
2931
+ req.params.arguments ?? {},
2932
+ String(extra.requestId)
2933
+ );
2934
+ if (own) return own;
2538
2935
  }
2539
- };
2540
- const handler = sessions ? createStatefulHandler(context, sessions) : createStatelessHandler(context);
2541
- const allow = sessions ? "GET, POST, DELETE, OPTIONS" : "POST, OPTIONS";
2542
- return async (request) => {
2543
- if (request.method === "OPTIONS") {
2544
- return new Response(null, { status: 204, headers: { Allow: allow, ...cors } });
2936
+ const { agent, error } = await agentFor(extra);
2937
+ const runId = runIdFor(extra, req.params._meta, opts.defaultRunId ?? runtime.defaultRunId);
2938
+ if (error) {
2939
+ return {
2940
+ isError: true,
2941
+ content: [{ type: "text", text: JSON.stringify(error) }],
2942
+ structuredContent: error,
2943
+ _meta: { httpStatus: 401 }
2944
+ };
2545
2945
  }
2546
- return handler(request);
2547
- };
2548
- }
2549
- function localhostHosts(port) {
2550
- const hosts = ["localhost", "127.0.0.1", "[::1]"];
2551
- return [...hosts, ...hosts.map((h) => `${h}:${port}`)];
2552
- }
2553
- function handleHealth(info) {
2554
- const tools = typeof info.tools === "number" ? info.tools : info.tools.length;
2555
- return jsonResponse({ ok: true, name: info.name, version: info.version, tools }, 200);
2556
- }
2557
- async function serveNodeHttp(opts) {
2558
- const http = await import("http");
2559
- const host = opts.host ?? "127.0.0.1";
2560
- const path2 = opts.path ?? "/mcp";
2561
- const server = http.createServer((req, res) => {
2562
- void (async () => {
2563
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? host}`);
2564
- let response;
2565
- if (url.pathname === path2) {
2566
- const controller = new AbortController();
2567
- res.on("close", () => {
2568
- if (!res.writableFinished) controller.abort();
2946
+ const progressToken = req.params._meta?.progressToken;
2947
+ const { result, tool } = await runtime.callTool(req.params.name, req.params.arguments ?? {}, {
2948
+ runId,
2949
+ agent,
2950
+ sessionId: extra.sessionId ?? session.id,
2951
+ session,
2952
+ requestId: extra.requestId,
2953
+ onprogress: progressToken === void 0 ? void 0 : (p) => {
2954
+ void extra.sendNotification({
2955
+ method: "notifications/progress",
2956
+ params: { progressToken, ...p }
2569
2957
  });
2570
- const headers = new Headers();
2571
- for (const [key, value] of Object.entries(req.headers)) {
2572
- if (Array.isArray(value)) for (const item of value) headers.append(key, item);
2573
- else if (typeof value === "string") headers.set(key, value);
2574
- }
2575
- const method = req.method ?? "GET";
2576
- let body;
2577
- if (method !== "GET" && method !== "HEAD") {
2578
- const chunks = [];
2579
- for await (const chunk of req) chunks.push(chunk);
2580
- body = new Uint8Array(Buffer.concat(chunks));
2581
- }
2582
- response = await opts.handler(
2583
- new Request(url, { method, headers, body, signal: controller.signal })
2584
- );
2585
- } else if (url.pathname === "/health" && opts.health) {
2586
- response = opts.health();
2587
- } else {
2588
- response = new Response(
2589
- JSON.stringify({
2590
- code: "NOT_FOUND",
2591
- cause: `no route for ${url.pathname}`,
2592
- fix: `POST JSON-RPC to ${path2}`,
2593
- retryable: false
2594
- }),
2595
- { status: 404, headers: { "Content-Type": "application/json" } }
2596
- );
2597
- }
2598
- const outHeaders = {};
2599
- response.headers.forEach((value, key) => {
2600
- outHeaders[key] = value;
2601
- });
2602
- res.writeHead(response.status, outHeaders);
2603
- if (!response.body) {
2604
- res.end();
2605
- return;
2606
- }
2607
- const reader = response.body.getReader();
2608
- try {
2609
- for (; ; ) {
2610
- const { done, value } = await reader.read();
2611
- if (done) break;
2612
- res.write(value);
2613
- }
2614
- } catch {
2615
2958
  }
2616
- res.end();
2617
- })().catch((err) => {
2618
- if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
2619
- res.end(
2620
- JSON.stringify({
2621
- code: "INTERNAL",
2622
- cause: err instanceof Error ? err.message : String(err),
2623
- fix: "retry; if it persists, report the request id",
2624
- retryable: false
2625
- })
2626
- );
2627
2959
  });
2960
+ return toCallToolResult2(result, tool);
2628
2961
  });
2629
- await new Promise((resolve5, reject) => {
2630
- server.once("error", reject);
2631
- server.listen(opts.port ?? 8765, host, () => resolve5());
2632
- });
2633
- const address = server.address();
2634
- const port = typeof address === "object" && address ? address.port : opts.port ?? 8765;
2635
- return {
2636
- port,
2637
- url: `http://${host}:${port}${path2}`,
2638
- close: () => new Promise((resolve5, reject) => {
2639
- server.closeAllConnections();
2640
- server.close((err) => err ? reject(err) : resolve5());
2641
- })
2962
+ const guardRead = async () => {
2963
+ const state = await runtime.guard.killState();
2964
+ if (state.killed) throw killedError(state);
2642
2965
  };
2643
- }
2644
-
2645
- // ../../packages/mcp-kit/dist/index.js
2646
- import { McpServer as McpServer3 } from "@modelcontextprotocol/sdk/server/mcp.js";
2647
- async function serveStdio(serverOrOpts) {
2648
- const server = serverOrOpts instanceof McpServer3 ? serverOrOpts : createMcpServer(serverOrOpts);
2649
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
2650
- const transport = new StdioServerTransport();
2651
- await server.connect(transport);
2652
- await new Promise((resolve5) => {
2653
- const previous = transport.onclose;
2654
- transport.onclose = () => {
2655
- previous?.();
2656
- resolve5();
2657
- };
2966
+ low.setRequestHandler(ListResourcesRequestSchema, async () => {
2967
+ touch();
2968
+ return runtime.listResources();
2969
+ });
2970
+ low.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
2971
+ touch();
2972
+ return runtime.listResourceTemplates();
2973
+ });
2974
+ low.setRequestHandler(ReadResourceRequestSchema, async (req) => {
2975
+ touch();
2976
+ await guardRead();
2977
+ return runtime.readResource(req.params.uri);
2978
+ });
2979
+ low.setRequestHandler(SubscribeRequestSchema2, async (req) => {
2980
+ await runtime.subscribe(req.params.uri, true);
2981
+ return {};
2982
+ });
2983
+ low.setRequestHandler(UnsubscribeRequestSchema2, async (req) => {
2984
+ await runtime.subscribe(req.params.uri, false);
2985
+ return {};
2986
+ });
2987
+ low.setRequestHandler(ListPromptsRequestSchema, async () => {
2988
+ touch();
2989
+ return runtime.listPrompts();
2990
+ });
2991
+ low.setRequestHandler(GetPromptRequestSchema, async (req) => {
2992
+ touch();
2993
+ await guardRead();
2994
+ return runtime.getPrompt(req.params.name, req.params.arguments);
2995
+ });
2996
+ low.setRequestHandler(CompleteRequestSchema, async (req) => {
2997
+ touch();
2998
+ return runtime.complete(req.params);
2999
+ });
3000
+ low.setRequestHandler(SetLevelRequestSchema, async (req) => {
3001
+ await runtime.setLevel(req.params.level);
3002
+ return {};
2658
3003
  });
3004
+ return server;
2659
3005
  }
2660
3006
 
2661
3007
  // src/proxy/http.ts
2662
- import { writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
3008
+ import { writeFileSync, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
2663
3009
  import { createServer } from "http";
2664
3010
  import { join } from "path";
2665
3011
  import { FileKillSwitch } from "@agentwares/agentguard-core/node";
@@ -2857,7 +3203,7 @@ async function startHttpProxy(opts) {
2857
3203
  port,
2858
3204
  token,
2859
3205
  close: () => new Promise((resolve5, reject) => {
2860
- if (opts.registerFile !== false && existsSync(regFile)) {
3206
+ if (opts.registerFile !== false && existsSync2(regFile)) {
2861
3207
  try {
2862
3208
  unlinkSync(regFile);
2863
3209
  } catch {
@@ -2878,7 +3224,7 @@ function unauthorized() {
2878
3224
  }
2879
3225
 
2880
3226
  // src/context.ts
2881
- import { existsSync as existsSync2 } from "fs";
3227
+ import { existsSync as existsSync3 } from "fs";
2882
3228
  import { resolve } from "path";
2883
3229
  import { Guard } from "@agentwares/agentguard-core";
2884
3230
  import {
@@ -2888,7 +3234,7 @@ import {
2888
3234
  FileStateStore,
2889
3235
  loadPolicyFile
2890
3236
  } from "@agentwares/agentguard-core/node";
2891
- import { existsSync as exists, readFileSync as readFileSync2 } from "fs";
3237
+ import { existsSync as exists, readFileSync as readFileSync3 } from "fs";
2892
3238
 
2893
3239
  // ../../node_modules/.pnpm/postal-mime@2.7.5/node_modules/postal-mime/src/decode-strings.js
2894
3240
  var textEncoder = new TextEncoder();
@@ -3272,7 +3618,7 @@ function readRegistration(loaded) {
3272
3618
  const file = registrationPath(loaded.stateDir);
3273
3619
  if (!exists(file)) return void 0;
3274
3620
  try {
3275
- const reg = JSON.parse(readFileSync2(file, "utf8"));
3621
+ const reg = JSON.parse(readFileSync3(file, "utf8"));
3276
3622
  return reg;
3277
3623
  } catch {
3278
3624
  return void 0;
@@ -3288,7 +3634,7 @@ function describeError(err) {
3288
3634
  }
3289
3635
 
3290
3636
  // src/configs.ts
3291
- import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3637
+ import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
3292
3638
  import { homedir as homedir2, platform } from "os";
3293
3639
  import { join as join2, resolve as resolve2 } from "path";
3294
3640
  function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.env) {
@@ -3321,6 +3667,12 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
3321
3667
  serversKey: "servers"
3322
3668
  },
3323
3669
  { kind: "generic", label: "mcp.json", path: join2(cwd, "mcp.json"), serversKey: "mcpServers" },
3670
+ {
3671
+ kind: "gemini-project",
3672
+ label: "Gemini CLI (project .gemini/settings.json)",
3673
+ path: join2(cwd, ".gemini", "settings.json"),
3674
+ serversKey: "mcpServers"
3675
+ },
3324
3676
  { kind: "claude-desktop", label: "Claude Desktop", path: desktop, serversKey: "mcpServers" },
3325
3677
  {
3326
3678
  kind: "cursor-user",
@@ -3339,11 +3691,20 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
3339
3691
  label: "Claude Code (user ~/.claude.json)",
3340
3692
  path: join2(home, ".claude.json"),
3341
3693
  serversKey: "mcpServers"
3694
+ },
3695
+ {
3696
+ kind: "gemini-user",
3697
+ label: "Gemini CLI (user ~/.gemini/settings.json)",
3698
+ path: join2(home, ".gemini", "settings.json"),
3699
+ serversKey: "mcpServers"
3342
3700
  }
3343
3701
  ];
3344
3702
  }
3703
+ function isGeminiConfig(file) {
3704
+ return file.kind === "gemini-project" || file.kind === "gemini-user" || /[/\\]\.gemini[/\\]settings\.json$/.test(file.path);
3705
+ }
3345
3706
  function detectClientConfigs(cwd, home, env) {
3346
- return candidateConfigs(cwd, home, env).filter((c) => existsSync3(c.path));
3707
+ return candidateConfigs(cwd, home, env).filter((c) => existsSync4(c.path));
3347
3708
  }
3348
3709
  function describeConfigPath(path2) {
3349
3710
  const abs = resolve2(path2);
@@ -3351,7 +3712,7 @@ function describeConfigPath(path2) {
3351
3712
  if (known) return known;
3352
3713
  let serversKey = "mcpServers";
3353
3714
  try {
3354
- const parsed = JSON.parse(readFileSync3(abs, "utf8"));
3715
+ const parsed = JSON.parse(readFileSync4(abs, "utf8"));
3355
3716
  if (parsed.servers && !parsed.mcpServers) serversKey = "servers";
3356
3717
  } catch {
3357
3718
  }
@@ -3363,7 +3724,7 @@ function describeConfigPath(path2) {
3363
3724
  };
3364
3725
  }
3365
3726
  function readServers(file) {
3366
- const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3727
+ const raw = JSON.parse(readFileSync4(file.path, "utf8"));
3367
3728
  const servers = raw[file.serversKey];
3368
3729
  if (!servers || typeof servers !== "object") return {};
3369
3730
  return servers;
@@ -3382,10 +3743,11 @@ function serversToUpstreams(servers) {
3382
3743
  continue;
3383
3744
  }
3384
3745
  const name = rawName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[^a-zA-Z0-9]+/, "") || "upstream";
3385
- if (entry.url) {
3746
+ const url = typeof entry.httpUrl === "string" ? entry.httpUrl : entry.url;
3747
+ if (url) {
3386
3748
  upstreams.push({
3387
3749
  name,
3388
- url: entry.url,
3750
+ url,
3389
3751
  headers: entry.headers ?? {},
3390
3752
  args: [],
3391
3753
  env: {},
@@ -3420,34 +3782,34 @@ function proxyEntry(file, policyPath, opts = {}) {
3420
3782
  return entry;
3421
3783
  }
3422
3784
  function rewriteClientConfig(file, policyPath, opts = {}) {
3423
- const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3785
+ const raw = JSON.parse(readFileSync4(file.path, "utf8"));
3424
3786
  const servers = raw[file.serversKey] ?? {};
3425
3787
  const replaced = Object.keys(servers).filter((n) => !isProxyEntry(servers[n]));
3426
3788
  const backup = backupPath(file);
3427
- if (!existsSync3(backup)) copyFileSync(file.path, backup);
3789
+ if (!existsSync4(backup)) copyFileSync(file.path, backup);
3428
3790
  raw[file.serversKey] = { [PROXY_SERVER_NAME]: proxyEntry(file, policyPath, opts) };
3429
3791
  writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
3430
3792
  return { backup, replaced };
3431
3793
  }
3432
3794
  function restoreClientConfig(file) {
3433
3795
  const backup = backupPath(file);
3434
- if (!existsSync3(backup)) return false;
3796
+ if (!existsSync4(backup)) return false;
3435
3797
  copyFileSync(backup, file.path);
3436
3798
  return true;
3437
3799
  }
3438
3800
  function mergeServerIntoConfig(file, name, entry) {
3439
- const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3801
+ const raw = JSON.parse(readFileSync4(file.path, "utf8"));
3440
3802
  const servers = raw[file.serversKey] ?? {};
3441
3803
  const replaced = name in servers;
3442
3804
  const backup = backupPath(file);
3443
- if (!existsSync3(backup)) copyFileSync(file.path, backup);
3805
+ if (!existsSync4(backup)) copyFileSync(file.path, backup);
3444
3806
  raw[file.serversKey] = { ...servers, [name]: entry };
3445
3807
  writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
3446
3808
  return { backup, replaced };
3447
3809
  }
3448
3810
 
3449
3811
  // src/commands/init.ts
3450
- import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
3812
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
3451
3813
  import { relative, resolve as resolve3 } from "path";
3452
3814
  import YAML from "yaml";
3453
3815
 
@@ -3534,6 +3896,10 @@ async function toolsCommand(args, io) {
3534
3896
  io.err(
3535
3897
  `${u.connected ? "ok " : "down"} ${u.name}: ${u.connected ? `${u.tools} tools` : u.error}`
3536
3898
  );
3899
+ if (result.upstreams.length === 0)
3900
+ io.err(
3901
+ "no upstreams in the policy: an MCP client sees agentguard's own read-only tools instead (agentguard_get_status, agentguard_get_report, agentguard_verify_audit_log)"
3902
+ );
3537
3903
  for (const line of formatToolTable(result.tools)) io.out(line);
3538
3904
  const counts = result.tools.reduce(
3539
3905
  (acc, t) => ({ ...acc, [t.classification.class]: (acc[t.classification.class] ?? 0) + 1 }),
@@ -3701,7 +4067,7 @@ async function initCommand(args, io) {
3701
4067
  `found ${c.path} (${c.label}, user-level) \u2014 not touched; pass --client ${JSON.stringify(c.path)} to route it through agentguard`
3702
4068
  );
3703
4069
  }
3704
- if (!chosen.every((c) => existsSync4(c.path))) {
4070
+ if (!chosen.every((c) => existsSync5(c.path))) {
3705
4071
  io.err(`no MCP config at ${chosen.map((c) => c.path).join(", ")}`);
3706
4072
  return 1;
3707
4073
  }
@@ -3716,11 +4082,11 @@ async function initCommand(args, io) {
3716
4082
  }
3717
4083
  if (chosen.length === 0)
3718
4084
  io.out(
3719
- "found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf) \u2014 writing a policy with no upstreams"
4085
+ "found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf, Gemini CLI) \u2014 writing a policy with no upstreams"
3720
4086
  );
3721
4087
  const { upstreams, skipped } = serversToUpstreams(servers);
3722
4088
  for (const s of skipped) io.err(`skipped ${s.name}: ${s.reason}`);
3723
- if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 && existsSync4(policyPath)) {
4089
+ if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 && existsSync5(policyPath)) {
3724
4090
  io.out(
3725
4091
  `already initialized: ${rel(policyPath)} exists and ${chosen.map((c) => rel(c.path)).join(", ")} already points at agentguard`
3726
4092
  );
@@ -3747,7 +4113,7 @@ async function initCommand(args, io) {
3747
4113
  }
3748
4114
  }
3749
4115
  const mode = flagString(args, "mode") ?? "dry-run";
3750
- if (existsSync4(policyPath) && !flagBool(args, "force")) {
4116
+ if (existsSync5(policyPath) && !flagBool(args, "force")) {
3751
4117
  io.out(`kept ${rel(policyPath)} (exists; pass --force to regenerate)`);
3752
4118
  } else {
3753
4119
  writeFileSync3(policyPath, renderStarterPolicy({ upstreams, tools: probed, mode, probeErrors }));
@@ -3875,7 +4241,7 @@ async function permissionDiffCommand(args, io) {
3875
4241
  }
3876
4242
 
3877
4243
  // src/fixtures/crm-server.ts
3878
- import { z as z5 } from "zod";
4244
+ import { z as z6 } from "zod";
3879
4245
  function seedState() {
3880
4246
  const contacts = /* @__PURE__ */ new Map();
3881
4247
  for (const [id, name, email] of [
@@ -3890,18 +4256,18 @@ function seedState() {
3890
4256
  var counter = 0;
3891
4257
  var nextId = (prefix) => `${prefix}_${(++counter).toString(36).padStart(4, "0")}`;
3892
4258
  function crmTools(state) {
3893
- const contactSchema = z5.object({
3894
- id: z5.string(),
3895
- name: z5.string(),
3896
- email: z5.string(),
3897
- updated_at: z5.string()
4259
+ const contactSchema = z6.object({
4260
+ id: z6.string(),
4261
+ name: z6.string(),
4262
+ email: z6.string(),
4263
+ updated_at: z6.string()
3898
4264
  });
3899
4265
  return [
3900
4266
  defineTool({
3901
4267
  name: "crm_list_contacts",
3902
4268
  description: "List every contact in the CRM. Read-only; returns id, name, email and updated_at.",
3903
- input: z5.object({ limit: z5.number().int().min(1).max(500).default(100) }),
3904
- output: z5.object({ contacts: z5.array(contactSchema), total: z5.number() }),
4269
+ input: z6.object({ limit: z6.number().int().min(1).max(500).default(100) }),
4270
+ output: z6.object({ contacts: z6.array(contactSchema), total: z6.number() }),
3905
4271
  annotations: { readOnlyHint: true },
3906
4272
  handler: ({ limit }) => ({
3907
4273
  contacts: [...state.contacts.values()].slice(0, limit),
@@ -3911,7 +4277,7 @@ function crmTools(state) {
3911
4277
  defineTool({
3912
4278
  name: "crm_get_contact",
3913
4279
  description: "Fetch one contact by id. Read-only; NOT_FOUND when the id does not exist.",
3914
- input: z5.object({ id: z5.string() }),
4280
+ input: z6.object({ id: z6.string() }),
3915
4281
  output: contactSchema,
3916
4282
  annotations: { readOnlyHint: true },
3917
4283
  handler: ({ id }) => {
@@ -3928,7 +4294,7 @@ function crmTools(state) {
3928
4294
  defineTool({
3929
4295
  name: "crm_search",
3930
4296
  description: "Search contacts by name or email substring. Returns matching contacts (no annotations on purpose).",
3931
- input: z5.object({ query: z5.string() }),
4297
+ input: z6.object({ query: z6.string() }),
3932
4298
  handler: ({ query }) => ({
3933
4299
  contacts: [...state.contacts.values()].filter(
3934
4300
  (c) => `${c.name} ${c.email}`.toLowerCase().includes(query.toLowerCase())
@@ -3938,8 +4304,8 @@ function crmTools(state) {
3938
4304
  defineTool({
3939
4305
  name: "crm_create_contact",
3940
4306
  description: "Create a contact. Returns the new id and created_at timestamp.",
3941
- input: z5.object({ name: z5.string(), email: z5.string() }),
3942
- output: z5.object({ id: z5.string(), created_at: z5.string() }),
4307
+ input: z6.object({ name: z6.string(), email: z6.string() }),
4308
+ output: z6.object({ id: z6.string(), created_at: z6.string() }),
3943
4309
  annotations: { readOnlyHint: false, destructiveHint: false },
3944
4310
  handler: ({ name, email }) => {
3945
4311
  const id = nextId("c");
@@ -3952,11 +4318,11 @@ function crmTools(state) {
3952
4318
  defineTool({
3953
4319
  name: "crm_update_contact",
3954
4320
  description: "Update fields on a contact. Returns the id and updated_at; NOT_FOUND for unknown ids.",
3955
- input: z5.object({
3956
- id: z5.string(),
3957
- fields: z5.object({ name: z5.string().optional(), email: z5.string().optional() })
4321
+ input: z6.object({
4322
+ id: z6.string(),
4323
+ fields: z6.object({ name: z6.string().optional(), email: z6.string().optional() })
3958
4324
  }),
3959
- output: z5.object({ id: z5.string(), updated_at: z5.string() }),
4325
+ output: z6.object({ id: z6.string(), updated_at: z6.string() }),
3960
4326
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
3961
4327
  handler: ({ id, fields }) => {
3962
4328
  const c = state.contacts.get(id);
@@ -3974,8 +4340,8 @@ function crmTools(state) {
3974
4340
  defineTool({
3975
4341
  name: "crm_delete_contact",
3976
4342
  description: "Permanently delete a contact. Irreversible. Returns { deleted, id }.",
3977
- input: z5.object({ id: z5.string() }),
3978
- output: z5.object({ deleted: z5.boolean(), id: z5.string() }),
4343
+ input: z6.object({ id: z6.string() }),
4344
+ output: z6.object({ deleted: z6.boolean(), id: z6.string() }),
3979
4345
  annotations: { readOnlyHint: false, destructiveHint: true },
3980
4346
  handler: ({ id }) => {
3981
4347
  const existed = state.contacts.delete(id);
@@ -3986,8 +4352,8 @@ function crmTools(state) {
3986
4352
  defineTool({
3987
4353
  name: "crm_send_email",
3988
4354
  description: "Send an email to a contact through the CRM. Returns the message id and sent_at.",
3989
- input: z5.object({ to: z5.string(), subject: z5.string(), body: z5.string() }),
3990
- output: z5.object({ message_id: z5.string(), sent_at: z5.string() }),
4355
+ input: z6.object({ to: z6.string(), subject: z6.string(), body: z6.string() }),
4356
+ output: z6.object({ message_id: z6.string(), sent_at: z6.string() }),
3991
4357
  handler: ({ to, subject }) => {
3992
4358
  state.emails.push({ to, subject });
3993
4359
  state.mutations.push({ tool: "crm_send_email", args: { to, subject } });
@@ -3997,12 +4363,12 @@ function crmTools(state) {
3997
4363
  defineTool({
3998
4364
  name: "crm_charge_card",
3999
4365
  description: "Charge a customer's card on file (amount in cents). Returns the charge id and amount_usd.",
4000
- input: z5.object({
4001
- customer_id: z5.string(),
4002
- amount_cents: z5.number().int().positive(),
4003
- currency: z5.string().default("usd")
4366
+ input: z6.object({
4367
+ customer_id: z6.string(),
4368
+ amount_cents: z6.number().int().positive(),
4369
+ currency: z6.string().default("usd")
4004
4370
  }),
4005
- output: z5.object({ charge_id: z5.string(), amount_usd: z5.number() }),
4371
+ output: z6.object({ charge_id: z6.string(), amount_usd: z6.number() }),
4006
4372
  handler: ({ customer_id, amount_cents }) => {
4007
4373
  const amount_usd = amount_cents / 100;
4008
4374
  state.charges.push({ customer_id, amount_usd });
@@ -4013,7 +4379,7 @@ function crmTools(state) {
4013
4379
  defineTool({
4014
4380
  name: "crm_frobnicate",
4015
4381
  description: "A tool with no annotations and no recognizable verb \u2014 exists to test `unknown` handling.",
4016
- input: z5.object({}),
4382
+ input: z6.object({}),
4017
4383
  handler: () => {
4018
4384
  state.mutations.push({ tool: "crm_frobnicate", args: {} });
4019
4385
  return { frobnicated: true };
@@ -4022,7 +4388,7 @@ function crmTools(state) {
4022
4388
  defineTool({
4023
4389
  name: "crm_fail",
4024
4390
  description: "Always returns a structured UPSTREAM_ERROR result, for error-path tests.",
4025
- input: z5.object({}),
4391
+ input: z6.object({}),
4026
4392
  annotations: { readOnlyHint: true },
4027
4393
  handler: () => {
4028
4394
  throw toolError("UPSTREAM_ERROR", "the CRM is having a bad day", "retry in a minute", {
@@ -4179,7 +4545,7 @@ ${steps.length} calls; halts: ${halts.join(", ") || "none"}. Now run: agentguard
4179
4545
  }
4180
4546
 
4181
4547
  // src/commands/connect.ts
4182
- import { existsSync as existsSync5 } from "fs";
4548
+ import { existsSync as existsSync6 } from "fs";
4183
4549
  import { relative as relative2 } from "path";
4184
4550
  var DEFAULT_PROXY_URL = "https://agentwares-agentguard-proxy.vercel.app";
4185
4551
  function fail(e) {
@@ -4257,11 +4623,12 @@ async function connectCommand(args, io, fetchImpl = fetch) {
4257
4623
  return 1;
4258
4624
  }
4259
4625
  for (const c of chosen) {
4260
- if (!existsSync5(c.path)) {
4626
+ if (!existsSync6(c.path)) {
4261
4627
  io.err(`no MCP config at ${c.path}`);
4262
4628
  return 1;
4263
4629
  }
4264
- const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, entry);
4630
+ const forClient = isGeminiConfig(c) ? { httpUrl: entry.url, ...entry.headers ? { headers: entry.headers } : {} } : entry;
4631
+ const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, forClient);
4265
4632
  io.out(
4266
4633
  `${replaced ? "updated" : "wrote "} ${rel(c.path)} (${c.label}) \u2192 ${PROXY_SERVER_NAME} = ${conn.mcpUrl}`
4267
4634
  );
@@ -4376,13 +4743,13 @@ async function approvalsCommand(args, io) {
4376
4743
  }
4377
4744
 
4378
4745
  // src/commands/keys.ts
4379
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4746
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4380
4747
  import YAML2, { isMap, isSeq, YAMLSeq } from "yaml";
4381
4748
  import { generateAgentKey, hashAgentKey } from "@agentwares/agentguard-core";
4382
4749
  async function keyCommand(args, io) {
4383
4750
  const [sub, name] = args.positionals;
4384
4751
  const path2 = policyPathFrom(args, io);
4385
- const doc = YAML2.parseDocument(readFileSync4(path2, "utf8"));
4752
+ const doc = YAML2.parseDocument(readFileSync5(path2, "utf8"));
4386
4753
  let agents = doc.get("agents");
4387
4754
  if (!isSeq(agents)) {
4388
4755
  agents = new YAMLSeq();
@@ -4465,14 +4832,65 @@ async function keyCommand(args, io) {
4465
4832
  }
4466
4833
 
4467
4834
  // src/commands/proxy.ts
4468
- import { resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
4835
+ import { existsSync as existsSync7 } from "fs";
4836
+ import { dirname } from "path";
4837
+ import { Guard as Guard3, defaultPolicy, resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
4838
+ import { FileKillSwitch as FileKillSwitch3, resolvePath } from "@agentwares/agentguard-core/node";
4469
4839
  function newRunId(now = /* @__PURE__ */ new Date()) {
4470
4840
  return `run_${now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}_${Math.random().toString(36).slice(2, 6)}`;
4471
4841
  }
4842
+ function unconfiguredPolicy(args, io) {
4843
+ if (flagString(args, "config") !== void 0 || io.env.AGENTGUARD_CONFIG) return void 0;
4844
+ const path2 = policyPathFrom(args, io);
4845
+ if (existsSync7(path2)) return void 0;
4846
+ const policy = defaultPolicy();
4847
+ const baseDir = dirname(path2);
4848
+ return {
4849
+ policy,
4850
+ path: path2,
4851
+ baseDir,
4852
+ stateDir: resolvePath(policy.state.dir, baseDir),
4853
+ auditPath: resolvePath(policy.audit.path, baseDir),
4854
+ killPath: resolvePath(policy.kill.file, baseDir)
4855
+ };
4856
+ }
4857
+ async function serveOverStdio(runtime) {
4858
+ const server = createDownstreamServer(runtime);
4859
+ const stop = async () => {
4860
+ await runtime.close();
4861
+ };
4862
+ process.once("SIGINT", () => void stop().then(() => process.exit(0)));
4863
+ process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
4864
+ await serveStdio(server);
4865
+ await stop();
4866
+ return 0;
4867
+ }
4472
4868
  async function proxyCommand(args, io) {
4473
- const loaded = loadPolicyFor(args, io);
4474
4869
  const log = (line) => io.err(`[agentguard] ${line}`);
4475
4870
  const modeOverride = flagString(args, "mode");
4871
+ const http = flagBool(args, "http") === true;
4872
+ const unconfigured = http ? void 0 : unconfiguredPolicy(args, io);
4873
+ if (unconfigured) {
4874
+ if (modeOverride === "dry-run" || modeOverride === "enforce")
4875
+ unconfigured.policy.mode = modeOverride;
4876
+ const runtime2 = new ProxyRuntime({
4877
+ loaded: unconfigured,
4878
+ guard: new Guard3({
4879
+ policy: unconfigured.policy,
4880
+ env: io.env,
4881
+ kill: new FileKillSwitch3(unconfigured.killPath)
4882
+ }),
4883
+ configured: false,
4884
+ defaultRunId: flagString(args, "run-id") ?? newRunId(),
4885
+ log
4886
+ });
4887
+ await runtime2.start();
4888
+ log(
4889
+ `no policy file at ${unconfigured.path}: serving agentguard's own read-only tools only. To guard your MCP servers, run \`${INIT_COMMAND}\` in the project directory.`
4890
+ );
4891
+ return serveOverStdio(runtime2);
4892
+ }
4893
+ const loaded = loadPolicyFor(args, io);
4476
4894
  if (modeOverride === "dry-run" || modeOverride === "enforce") loaded.policy.mode = modeOverride;
4477
4895
  const agentName = flagString(args, "agent");
4478
4896
  let agent = void 0;
@@ -4486,7 +4904,6 @@ async function proxyCommand(args, io) {
4486
4904
  }
4487
4905
  agent = resolved.agent;
4488
4906
  }
4489
- const http = flagBool(args, "http") === true;
4490
4907
  let approvalBase;
4491
4908
  const fileGuard = createFileGuard(loaded, {
4492
4909
  log,
@@ -4512,59 +4929,55 @@ async function proxyCommand(args, io) {
4512
4929
  log(
4513
4930
  `kill switch is ON (${killed.reason}); every call returns KILLED until \`agentguard resume\``
4514
4931
  );
4932
+ if (runtime.upstreamStatus().length === 0)
4933
+ log(
4934
+ `${loaded.path} lists no upstreams: serving agentguard's own read-only tools only. Add servers under \`upstreams:\` to guard them.`
4935
+ );
4515
4936
  if (http) {
4516
- const server2 = await startHttpProxy({
4937
+ const server = await startHttpProxy({
4517
4938
  runtime,
4518
4939
  port: flagNumber(args, "port") ?? 8788,
4519
4940
  host: flagString(args, "host") ?? "127.0.0.1",
4520
4941
  token: io.env.AGENTGUARD_HTTP_TOKEN
4521
4942
  });
4522
- approvalBase = { baseUrl: server2.url.replace(/\/mcp$/, ""), token: server2.token };
4943
+ approvalBase = { baseUrl: server.url.replace(/\/mcp$/, ""), token: server.token };
4523
4944
  log(
4524
- `Streamable HTTP at ${server2.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
4945
+ `Streamable HTTP at ${server.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
4525
4946
  );
4526
4947
  log(
4527
- `point your agent at ${server2.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
4948
+ `point your agent at ${server.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
4528
4949
  );
4529
- const stop2 = async () => {
4530
- await server2.close();
4950
+ const stop = async () => {
4951
+ await server.close();
4531
4952
  await runtime.close();
4532
4953
  process.exit(0);
4533
4954
  };
4534
- process.once("SIGINT", () => void stop2());
4535
- process.once("SIGTERM", () => void stop2());
4955
+ process.once("SIGINT", () => void stop());
4956
+ process.once("SIGTERM", () => void stop());
4536
4957
  await new Promise(() => void 0);
4537
4958
  }
4538
- const server = createDownstreamServer(runtime);
4539
- const stop = async () => {
4540
- await runtime.close();
4541
- };
4542
- process.once("SIGINT", () => void stop().then(() => process.exit(0)));
4543
- process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
4544
- await serveStdio(server);
4545
- await stop();
4546
- return 0;
4959
+ return serveOverStdio(runtime);
4547
4960
  }
4548
4961
 
4549
4962
  // src/commands/report.ts
4550
4963
  import { resolve as resolve4 } from "path";
4551
4964
  import {
4552
- buildReport,
4553
- latestRunId,
4554
- listRuns,
4965
+ buildReport as buildReport2,
4966
+ latestRunId as latestRunId2,
4967
+ listRuns as listRuns2,
4555
4968
  renderMutationDiff,
4556
- renderReportMarkdown
4969
+ renderReportMarkdown as renderReportMarkdown2
4557
4970
  } from "@agentwares/agentguard-core";
4558
4971
  import { readAuditFile, verifyAuditFile } from "@agentwares/agentguard-core/node";
4559
4972
  function pickRun(args, entries) {
4560
4973
  if (flagBool(args, "all")) return void 0;
4561
- return flagString(args, "run") ?? latestRunId(entries);
4974
+ return flagString(args, "run") ?? latestRunId2(entries);
4562
4975
  }
4563
4976
  async function reportCommand(args, io) {
4564
4977
  const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
4565
4978
  const entries = readAuditFile(loaded.auditPath);
4566
4979
  const runId = pickRun(args, entries);
4567
- const report = buildReport(entries, { runId });
4980
+ const report = buildReport2(entries, { runId });
4568
4981
  if (flagBool(args, "json")) {
4569
4982
  io.out(
4570
4983
  JSON.stringify(
@@ -4583,12 +4996,12 @@ async function reportCommand(args, io) {
4583
4996
  );
4584
4997
  return 0;
4585
4998
  }
4586
- io.out(renderReportMarkdown(report));
4999
+ io.out(renderReportMarkdown2(report));
4587
5000
  const chain = await verifyAuditFile(loaded.auditPath);
4588
5001
  io.out(
4589
5002
  chain.ok ? `Audit chain: ${chain.entries} entries, verified (${loaded.auditPath}).` : `Audit chain BROKEN at entry ${chain.brokenAt}: ${chain.reason}`
4590
5003
  );
4591
- if (runId && listRuns(entries).length > 1) io.out(`Other runs: agentguard report --all`);
5004
+ if (runId && listRuns2(entries).length > 1) io.out(`Other runs: agentguard report --all`);
4592
5005
  return 0;
4593
5006
  }
4594
5007
  async function diffCommand(args, io) {
@@ -4615,7 +5028,7 @@ async function statusCommand(args, io) {
4615
5028
  const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
4616
5029
  const { guard, kill } = createFileGuard(loaded, { alerts: false, env: io.env });
4617
5030
  const entries = readAuditFile(loaded.auditPath);
4618
- const runId = flagString(args, "run") ?? latestRunId(entries) ?? "(none yet)";
5031
+ const runId = flagString(args, "run") ?? latestRunId2(entries) ?? "(none yet)";
4619
5032
  const status = await guard.status(runId);
4620
5033
  const killed = kill.check();
4621
5034
  const reg = readRegistration(loaded);
@@ -4784,6 +5197,6 @@ export {
4784
5197
  seedState,
4785
5198
  serversToUpstreams,
4786
5199
  startHttpProxy,
4787
- toCallToolResult
5200
+ toCallToolResult2 as toCallToolResult
4788
5201
  };
4789
5202
  //# sourceMappingURL=index.js.map