@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/CHANGELOG.md +83 -0
- package/README.md +69 -19
- package/dist/cli.js +476 -65
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +2197 -1784
- package/dist/index.js.map +1 -1
- package/llms.txt +4 -0
- package/package.json +7 -2
- package/server.json +3 -3
package/dist/index.js
CHANGED
|
@@ -840,1826 +840,2172 @@ import {
|
|
|
840
840
|
classifyTool,
|
|
841
841
|
upstreamHeaders
|
|
842
842
|
} from "@agentwares/agentguard-core";
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
870
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
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
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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 {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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/
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
{
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
|
|
1631
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1827
|
-
|
|
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
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
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
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
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
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
{
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
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
|
|
2125
|
-
|
|
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
|
|
2128
|
-
if (!
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
if (
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
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
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
-
|
|
2203
|
-
|
|
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
|
-
|
|
2208
|
-
|
|
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
|
|
2248
|
-
|
|
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
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
return
|
|
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
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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
|
|
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
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
|
2419
|
-
|
|
2420
|
-
|
|
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
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
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
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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
|
|
2471
|
-
|
|
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
|
-
|
|
2475
|
-
const
|
|
2476
|
-
if (
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
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
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
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
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
-
|
|
2646
|
-
|
|
2647
|
-
async
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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) =>
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3746
|
+
const url = typeof entry.httpUrl === "string" ? entry.httpUrl : entry.url;
|
|
3747
|
+
if (url) {
|
|
3386
3748
|
upstreams.push({
|
|
3387
3749
|
name,
|
|
3388
|
-
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(
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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
|
|
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) =>
|
|
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 &&
|
|
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 (
|
|
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
|
|
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 =
|
|
3894
|
-
id:
|
|
3895
|
-
name:
|
|
3896
|
-
email:
|
|
3897
|
-
updated_at:
|
|
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:
|
|
3904
|
-
output:
|
|
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:
|
|
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:
|
|
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:
|
|
3942
|
-
output:
|
|
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:
|
|
3956
|
-
id:
|
|
3957
|
-
fields:
|
|
4321
|
+
input: z6.object({
|
|
4322
|
+
id: z6.string(),
|
|
4323
|
+
fields: z6.object({ name: z6.string().optional(), email: z6.string().optional() })
|
|
3958
4324
|
}),
|
|
3959
|
-
output:
|
|
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:
|
|
3978
|
-
output:
|
|
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:
|
|
3990
|
-
output:
|
|
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:
|
|
4001
|
-
customer_id:
|
|
4002
|
-
amount_cents:
|
|
4003
|
-
currency:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 (!
|
|
4626
|
+
if (!existsSync6(c.path)) {
|
|
4261
4627
|
io.err(`no MCP config at ${c.path}`);
|
|
4262
4628
|
return 1;
|
|
4263
4629
|
}
|
|
4264
|
-
const {
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
|
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:
|
|
4943
|
+
approvalBase = { baseUrl: server.url.replace(/\/mcp$/, ""), token: server.token };
|
|
4523
4944
|
log(
|
|
4524
|
-
`Streamable HTTP at ${
|
|
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 ${
|
|
4948
|
+
`point your agent at ${server.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
|
|
4528
4949
|
);
|
|
4529
|
-
const
|
|
4530
|
-
await
|
|
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
|
|
4535
|
-
process.once("SIGTERM", () => void
|
|
4955
|
+
process.once("SIGINT", () => void stop());
|
|
4956
|
+
process.once("SIGTERM", () => void stop());
|
|
4536
4957
|
await new Promise(() => void 0);
|
|
4537
4958
|
}
|
|
4538
|
-
|
|
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") ??
|
|
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 =
|
|
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(
|
|
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 &&
|
|
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") ??
|
|
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
|