@aimarket/warden 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +155 -3
  2. package/README-es.md +82 -11
  3. package/README-fr.md +82 -11
  4. package/README-ru.md +82 -11
  5. package/README-zh.md +79 -10
  6. package/README.md +91 -14
  7. package/dist/glob.d.ts +81 -0
  8. package/dist/glob.d.ts.map +1 -0
  9. package/dist/glob.js +180 -0
  10. package/dist/glob.js.map +1 -0
  11. package/dist/index.d.ts +42 -9
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +84 -29
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp-rpc.d.ts +40 -0
  16. package/dist/mcp-rpc.d.ts.map +1 -0
  17. package/dist/mcp-rpc.js +117 -0
  18. package/dist/mcp-rpc.js.map +1 -0
  19. package/dist/mcp-server.d.ts +10 -0
  20. package/dist/mcp-server.d.ts.map +1 -0
  21. package/dist/mcp-server.js +89 -0
  22. package/dist/mcp-server.js.map +1 -0
  23. package/dist/mcp-tools.d.ts +42 -0
  24. package/dist/mcp-tools.d.ts.map +1 -0
  25. package/dist/mcp-tools.js +652 -0
  26. package/dist/mcp-tools.js.map +1 -0
  27. package/dist/origin.d.ts.map +1 -1
  28. package/dist/origin.js +7 -2
  29. package/dist/origin.js.map +1 -1
  30. package/dist/pinning.d.ts +18 -0
  31. package/dist/pinning.d.ts.map +1 -1
  32. package/dist/pinning.js +59 -11
  33. package/dist/pinning.js.map +1 -1
  34. package/dist/sandbox.d.ts.map +1 -1
  35. package/dist/sandbox.js +10 -7
  36. package/dist/sandbox.js.map +1 -1
  37. package/dist/sanitize.d.ts +38 -0
  38. package/dist/sanitize.d.ts.map +1 -0
  39. package/dist/sanitize.js +53 -0
  40. package/dist/sanitize.js.map +1 -0
  41. package/dist/static-scan.d.ts +46 -2
  42. package/dist/static-scan.d.ts.map +1 -1
  43. package/dist/static-scan.js +358 -34
  44. package/dist/static-scan.js.map +1 -1
  45. package/dist/threat-feed.d.ts +3 -0
  46. package/dist/threat-feed.d.ts.map +1 -1
  47. package/dist/threat-feed.js +118 -29
  48. package/dist/threat-feed.js.map +1 -1
  49. package/dist/types.d.ts +15 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +11 -4
@@ -0,0 +1,652 @@
1
+ /**
2
+ * MCP tool surface for the WARDEN stdio server.
3
+ *
4
+ * These definitions are scored by Glama TDQS. Each tool: a title longer than
5
+ * the name, when-to-use / when-not naming a sibling, MCP annotations that match
6
+ * behaviour, every input property described, and an output schema so the
7
+ * description does not have to narrate the return shape.
8
+ *
9
+ * All six tools are local, deterministic, and perform no network I/O. The
10
+ * stdio process uses the built-in threat-feed floor and an empty pin store —
11
+ * it is a scanner, not a long-lived host.
12
+ */
13
+ import { CanonicalizationError, EgressGuard, StaticScanGate, ThreatFeed, Warden, canonicalize, classifyTools, parseJsonStrict, staticScanRuleset, } from "./index.js";
14
+ export const MAX_TOOLS = 256;
15
+ export const MAX_STRING = 16_384;
16
+ export const MAX_PAYLOAD_CHARS = 256_000;
17
+ const SEVERITIES = ["info", "low", "medium", "high", "critical"];
18
+ export const DEFAULT_MCP_POLICY = {
19
+ blockAtSeverity: "high",
20
+ sensitiveToolPatterns: [],
21
+ allowUnknownServers: true,
22
+ pinToolDefs: true,
23
+ };
24
+ const TOOL_DEF_ITEM = {
25
+ type: "object",
26
+ additionalProperties: false,
27
+ required: ["name", "description", "inputSchema"],
28
+ properties: {
29
+ name: {
30
+ type: "string",
31
+ minLength: 1,
32
+ maxLength: 256,
33
+ description: "Exact tool name as advertised by tools/list. Matched case-sensitively against findings.tool and against sensitive glob patterns.",
34
+ },
35
+ description: {
36
+ type: "string",
37
+ maxLength: MAX_STRING,
38
+ description: "Advertised description string from tools/list. This is prompt text the model would see; WARDEN scans it. Pass the server's text unmodified — rewriting it hides the injection surface.",
39
+ },
40
+ inputSchema: {
41
+ type: "object",
42
+ description: "JSON Schema object from tools/list (type/properties/required/…). Pass through as-is. WARDEN stringifies it and scans field names, descriptions, and enums. Do not invent properties the server did not advertise.",
43
+ },
44
+ },
45
+ };
46
+ const FINDING_SCHEMA = {
47
+ type: "object",
48
+ additionalProperties: false,
49
+ required: ["gate", "severity", "code", "message"],
50
+ properties: {
51
+ gate: { type: "string", description: "Gate that produced the finding: static-scan, threat-feed, origin, or pinning." },
52
+ severity: { type: "string", enum: SEVERITIES, description: "Attention rank. Independent of whether the finding blocks." },
53
+ code: { type: "string", description: "Stable machine code, e.g. TOOL_DEF_INJECTION or THREAT_TOOL_MATCH." },
54
+ message: { type: "string", description: "Human-readable finding, including the matched span where a static-scan rule fired." },
55
+ tool: { type: "string", description: "Tool name the finding refers to, when the defect is per-tool rather than server-wide." },
56
+ advisory: {
57
+ type: "boolean",
58
+ description: "When true the finding is report-only: it never blocks and never costs a tool, at any blockAtSeverity.",
59
+ },
60
+ },
61
+ };
62
+ const RULESET_REF_SCHEMA = {
63
+ type: "object",
64
+ additionalProperties: false,
65
+ required: ["version", "digest"],
66
+ properties: {
67
+ version: { type: "string", description: "Monotonic static-scan ruleset version (currently \"4\")." },
68
+ digest: {
69
+ type: "string",
70
+ description: "sha256-<base64> over the RFC 8785 canonical form of the published rule table. A recorded scan is not reproducible without this.",
71
+ },
72
+ },
73
+ };
74
+ export const MCP_TOOLS = [
75
+ {
76
+ name: "vet_mcp_server",
77
+ title: "Vet an MCP server through the full WARDEN gate chain",
78
+ description: "Run WARDEN's ordered gate chain (static-scan → threat-feed → origin → pinning) over a server identity plus its advertised tools/list payload and return a recordable verdict (allow/block, 0..1 product score, findings, allowedTools/blockedTools, ruleset digest).\n\n" +
79
+ "When to use: you have a complete server record and want the same decision a host should make before any of those tool definitions reach the model. Prefer this over calling the four gates yourself.\n\n" +
80
+ "When NOT to use: inspecting descriptions only (call static_scan_tools — no origin/pinning); splitting tools by operator glob (classify_sensitive_tools); checking one outbound URL (check_egress_url); producing RFC 8785 bytes (canonicalize_json).\n\n" +
81
+ "Behaviour: local, deterministic, no network. This stdio process uses the built-in 11-record threat floor (it does not fetch a signed feed) and an empty in-memory pin store, so every server is first-contact: TOOL_DEF_UNPINNED is advisory and does not block. Origin defaults to allowUnknownServers=true so catalog-discovered servers are not fail-closed. Override policy when you need the host's real knobs. Does not connect to, start, or approve the target server.\n\n" +
82
+ "Returns structured JSON matching outputSchema. Example: vet_mcp_server({ server: { id: \"demo@0\", name: \"demo\", transport: \"stdio\", command: \"npx\" }, tools: [{ name: \"add\", description: \"Add two integers.\", inputSchema: { type: \"object\" } }] }).",
83
+ annotations: {
84
+ title: "Vet an MCP server through the full WARDEN gate chain",
85
+ readOnlyHint: true,
86
+ destructiveHint: false,
87
+ idempotentHint: true,
88
+ openWorldHint: false,
89
+ },
90
+ inputSchema: {
91
+ type: "object",
92
+ additionalProperties: false,
93
+ required: ["server", "tools"],
94
+ properties: {
95
+ server: {
96
+ type: "object",
97
+ additionalProperties: false,
98
+ required: ["id", "name", "transport"],
99
+ description: "Identity of the MCP server being vetted — the host's McpServerRef, not a live connection. WARDEN never launches this command or fetches this URL.",
100
+ properties: {
101
+ id: {
102
+ type: "string",
103
+ minLength: 1,
104
+ maxLength: 256,
105
+ description: "Stable server id the host uses for pins and logs (e.g. \"github@1\"). Findings quote this id.",
106
+ },
107
+ name: {
108
+ type: "string",
109
+ minLength: 1,
110
+ maxLength: 256,
111
+ description: "Human-facing server name. Threat-feed server-scope patterns match this field.",
112
+ },
113
+ transport: {
114
+ type: "string",
115
+ enum: ["stdio", "sse", "http"],
116
+ description: "How the host would launch the server. stdio uses command/args; sse/http use url. WARDEN does not open the transport.",
117
+ },
118
+ command: {
119
+ type: "string",
120
+ maxLength: 1024,
121
+ description: "stdio executable (e.g. \"npx\"). Required for a realistic stdio identity hash; omit for url transports.",
122
+ },
123
+ args: {
124
+ type: "array",
125
+ items: { type: "string", maxLength: 1024 },
126
+ maxItems: 32,
127
+ description: "stdio argv. Included in the launch-identity hash used by pinning. Omit when unused.",
128
+ },
129
+ url: {
130
+ type: "string",
131
+ maxLength: 2048,
132
+ description: "sse/http endpoint. Threat-feed and identity hashing see this string; this tool does not request it.",
133
+ },
134
+ catalog: {
135
+ type: "string",
136
+ maxLength: 256,
137
+ description: "Remote catalog this server was discovered from. When set and policy.allowUnknownServers is false, the origin gate fail-closes. Omit for operator-declared servers.",
138
+ },
139
+ },
140
+ },
141
+ tools: {
142
+ type: "array",
143
+ minItems: 1,
144
+ maxItems: MAX_TOOLS,
145
+ description: "Exact tools/list payload (1..256 tools). Each item is name + description + inputSchema from the server. Do not filter before vetting — blockedTools is the partition.",
146
+ items: TOOL_DEF_ITEM,
147
+ },
148
+ policy: {
149
+ type: "object",
150
+ additionalProperties: false,
151
+ description: "Optional WardenPolicy overlay. Omitted keys keep the stdio defaults: blockAtSeverity=high, empty sensitiveToolPatterns, allowUnknownServers=true, pinToolDefs=true. Use classify_sensitive_tools when you only want the glob split.",
152
+ properties: {
153
+ blockAtSeverity: {
154
+ type: "string",
155
+ enum: SEVERITIES,
156
+ description: "Block the whole connection when any non-advisory finding reaches this severity. Default high. advisory findings never block, even at info.",
157
+ },
158
+ sensitiveToolPatterns: {
159
+ type: "array",
160
+ items: { type: "string", maxLength: 128 },
161
+ maxItems: 64,
162
+ description: "Case-insensitive * globs matched against tool names. Hits are still advertised; they go to blockedTools only when a finding also trips. For a glob-only split, call classify_sensitive_tools.",
163
+ },
164
+ allowUnknownServers: {
165
+ type: "boolean",
166
+ description: "When false, servers that carry server.catalog are refused by the origin gate. Default true on this stdio scanner so you can inspect catalog entries without declaring them.",
167
+ },
168
+ pinToolDefs: {
169
+ type: "boolean",
170
+ description: "When true, tool-def drift after a stored pin is fatal. This process has an empty pin store, so first contact stays UNPINNED/advisory. Default true.",
171
+ },
172
+ },
173
+ },
174
+ },
175
+ },
176
+ outputSchema: {
177
+ type: "object",
178
+ additionalProperties: false,
179
+ required: ["allow", "score", "findings", "allowedTools", "blockedTools", "rulesets"],
180
+ properties: {
181
+ allow: { type: "boolean", description: "false when a gate was fatal or a non-advisory finding reached blockAtSeverity." },
182
+ score: {
183
+ type: "number",
184
+ minimum: 0,
185
+ maximum: 1,
186
+ description: "Product of per-gate scores in [0, 1]. One bad gate drags the whole server down; it is not an average.",
187
+ },
188
+ decidedBy: { type: "string", description: "Gate that produced the blocking decision, present only when allow is false." },
189
+ findings: { type: "array", items: FINDING_SCHEMA, description: "Accumulated findings across all gates, including advisory." },
190
+ allowedTools: { type: "array", items: { type: "string" }, description: "Tool names the host may expose to the model." },
191
+ blockedTools: { type: "array", items: { type: "string" }, description: "Tool names quarantined; the rest of the server may still be usable." },
192
+ rulesets: {
193
+ type: "object",
194
+ additionalProperties: false,
195
+ required: ["staticScan"],
196
+ properties: { staticScan: RULESET_REF_SCHEMA },
197
+ description: "Rule-table identity in force for this verdict. Store this with the scan.",
198
+ },
199
+ },
200
+ },
201
+ },
202
+ {
203
+ name: "static_scan_tools",
204
+ title: "Static-scan MCP tool definitions for injection and exfil",
205
+ description: "Run only the static-scan gate (ruleset v4, 25 signatures with context guards) over advertised tool names, descriptions, and input schemas. Returns findings, a 0..1 gate score, and the published ruleset digest.\n\n" +
206
+ "When to use: you have a tools/list dump and want injection / credential / hidden-Unicode hits without origin, pinning, or the threat feed. Cheaper and narrower than vet_mcp_server.\n\n" +
207
+ "When NOT to use: you need the full host decision (vet_mcp_server); you want operator glob classification (classify_sensitive_tools); you want the published rule table itself (list_scan_rules).\n\n" +
208
+ "Behaviour: local regex+guard evaluation, no network, no mutation. Advisory-tier hits are reported with advisory=true and do not reduce the score. Does not launch servers or send tool output to a model.\n\n" +
209
+ "Returns structured JSON matching outputSchema. Example: static_scan_tools({ tools: [{ name: \"add\", description: \"Add two integers.\", inputSchema: { type: \"object\" } }] }).",
210
+ annotations: {
211
+ title: "Static-scan MCP tool definitions for injection and exfil",
212
+ readOnlyHint: true,
213
+ destructiveHint: false,
214
+ idempotentHint: true,
215
+ openWorldHint: false,
216
+ },
217
+ inputSchema: {
218
+ type: "object",
219
+ additionalProperties: false,
220
+ required: ["tools"],
221
+ properties: {
222
+ tools: {
223
+ type: "array",
224
+ minItems: 1,
225
+ maxItems: MAX_TOOLS,
226
+ description: "tools/list items to scan (1..256). Same shape as vet_mcp_server.tools.",
227
+ items: TOOL_DEF_ITEM,
228
+ },
229
+ },
230
+ },
231
+ outputSchema: {
232
+ type: "object",
233
+ additionalProperties: false,
234
+ required: ["score", "findings", "ruleset"],
235
+ properties: {
236
+ score: {
237
+ type: "number",
238
+ minimum: 0,
239
+ maximum: 1,
240
+ description: "static-scan gate contribution: 1 minus the penalty for the worst non-advisory severity. Advisory hits do not change this number.",
241
+ },
242
+ findings: { type: "array", items: FINDING_SCHEMA, description: "Hits from the 25-rule table, including advisory-only codes." },
243
+ ruleset: RULESET_REF_SCHEMA,
244
+ },
245
+ },
246
+ },
247
+ {
248
+ name: "classify_sensitive_tools",
249
+ title: "Classify MCP tools as sensitive vs safe by glob policy",
250
+ description: "Split advertised tool names into sensitive vs safe using the operator's case-insensitive * globs (the same policy.sensitiveToolPatterns a host would use). Sensitive tools stay advertised; they require per-call approval — this tool does not run them.\n\n" +
251
+ "When to use: show the user which names will need confirmation before they approve a server, or to preview a glob set. This is policy over identifiers, not an injection scan.\n\n" +
252
+ "When NOT to use: scanning descriptions for poisoning (static_scan_tools or vet_mcp_server); checking whether a URL is allowed out (check_egress_url).\n\n" +
253
+ "Behaviour: local glob match, no network. An empty patterns array marks every tool safe. Patterns match the whole name; \"*delete*\" hits create_delete_repo. Does not call Warden.vet and does not persist anything.\n\n" +
254
+ "Returns { sensitive, safe }. Example: classify_sensitive_tools({ tools: [{ name: \"delete_repo\", description: \"Delete a repository.\", inputSchema: { type: \"object\" } }], patterns: [\"*delete*\"] }).",
255
+ annotations: {
256
+ title: "Classify MCP tools as sensitive vs safe by glob policy",
257
+ readOnlyHint: true,
258
+ destructiveHint: false,
259
+ idempotentHint: true,
260
+ openWorldHint: false,
261
+ },
262
+ inputSchema: {
263
+ type: "object",
264
+ additionalProperties: false,
265
+ required: ["tools", "patterns"],
266
+ properties: {
267
+ tools: {
268
+ type: "array",
269
+ minItems: 1,
270
+ maxItems: MAX_TOOLS,
271
+ description: "tools/list items whose names will be classified. Descriptions and schemas are ignored; only name is matched.",
272
+ items: TOOL_DEF_ITEM,
273
+ },
274
+ patterns: {
275
+ type: "array",
276
+ items: { type: "string", minLength: 1, maxLength: 128 },
277
+ maxItems: 64,
278
+ description: "Operator globs, case-insensitive, matched against the whole tool name. \"*\" is the only wildcard. Empty array → every tool is safe. Same semantics as WardenPolicy.sensitiveToolPatterns, not threat-feed matching.",
279
+ },
280
+ },
281
+ },
282
+ outputSchema: {
283
+ type: "object",
284
+ additionalProperties: false,
285
+ required: ["sensitive", "safe"],
286
+ properties: {
287
+ sensitive: { type: "array", items: { type: "string" }, description: "Names matching at least one pattern. Still advertised; require per-call approval." },
288
+ safe: { type: "array", items: { type: "string" }, description: "Names matching no pattern." },
289
+ },
290
+ },
291
+ },
292
+ {
293
+ name: "check_egress_url",
294
+ title: "Check a URL against a WARDEN egress allowlist",
295
+ description: "Ask EgressGuard whether a URL's hostname is on an operator allowlist. A tool reaching a host you never listed is the classic phone-home tell. Empty allowlist blocks everything (fail-closed), not everything-allowed.\n\n" +
296
+ "When to use: a tool is about to fetch/post and you want the same check a host should wrap around that request. Hostnames match case-insensitively; \"*.example.com\" matches subdomains, not the apex.\n\n" +
297
+ "When NOT to use: vetting tool definitions (vet_mcp_server / static_scan_tools); canonicalizing JSON (canonicalize_json). This does not fetch the URL and does not inspect tool text.\n\n" +
298
+ "Behaviour: local URL parse + hostname match. Unparseable URLs are refused. No DNS, no HTTP. Idempotent.\n\n" +
299
+ "Returns { allowed, host?, reason? }. Example: check_egress_url({ url: \"https://api.github.com/repos\", allowlist: [\"api.github.com\", \"*.internal.example.com\"] }).",
300
+ annotations: {
301
+ title: "Check a URL against a WARDEN egress allowlist",
302
+ readOnlyHint: true,
303
+ destructiveHint: false,
304
+ idempotentHint: true,
305
+ openWorldHint: false,
306
+ },
307
+ inputSchema: {
308
+ type: "object",
309
+ additionalProperties: false,
310
+ required: ["url", "allowlist"],
311
+ properties: {
312
+ url: {
313
+ type: "string",
314
+ minLength: 1,
315
+ maxLength: 2048,
316
+ description: "Absolute URL the tool wants to open (https://host/path). Only the hostname is compared; path, query, and credentials are not allowlisted separately. Must be parseable by the WHATWG URL parser.",
317
+ },
318
+ allowlist: {
319
+ type: "array",
320
+ items: { type: "string", maxLength: 253 },
321
+ maxItems: 256,
322
+ description: "Permitted hostnames. Exact match, or leading \"*.\" for subdomains (\"*.example.com\" matches api.example.com, not example.com). Empty array blocks every host. Entries are trimmed and compared case-insensitively.",
323
+ },
324
+ },
325
+ },
326
+ outputSchema: {
327
+ type: "object",
328
+ additionalProperties: false,
329
+ required: ["allowed"],
330
+ properties: {
331
+ allowed: { type: "boolean", description: "true only when the hostname matches an allowlist entry." },
332
+ host: { type: "string", description: "Parsed hostname when the URL was valid." },
333
+ reason: { type: "string", description: "Why the request is blocked; omitted when allowed is true." },
334
+ },
335
+ },
336
+ },
337
+ {
338
+ name: "canonicalize_json",
339
+ title: "Canonicalize JSON with RFC 8785 (JCS) bytes",
340
+ description: "Return the RFC 8785 JSON Canonicalization Scheme serialization WARDEN uses for threat-feed signatures and tool-def pins, so another implementation can byte-check against it. Integers only inside ±(2^53−1); lone surrogates and non-integers are refused with a reason code, not escaped.\n\n" +
341
+ "When to use: you are publishing or verifying a signed threat feed, hashing tool defs, or comparing two JSON documents that must agree regardless of key order. Subpath @aimarket/warden/jcs is the same function.\n\n" +
342
+ "When NOT to use: scanning tool defs (static_scan_tools); pretty-printing for humans (this output is for bytes, not display).\n\n" +
343
+ "Behaviour: local, no network. Pass either a parsed JSON value or a JSON string (string is parsed with parseJsonStrict first). Failure returns isError with CanonicalizationCode — it does not emit partial bytes.\n\n" +
344
+ "Returns { canonical } or an error. Example: canonicalize_json({ value: { b: 1, a: 2 } }) → {\"a\":2,\"b\":1}.",
345
+ annotations: {
346
+ title: "Canonicalize JSON with RFC 8785 (JCS) bytes",
347
+ readOnlyHint: true,
348
+ destructiveHint: false,
349
+ idempotentHint: true,
350
+ openWorldHint: false,
351
+ },
352
+ inputSchema: {
353
+ type: "object",
354
+ additionalProperties: false,
355
+ required: ["value"],
356
+ properties: {
357
+ value: {
358
+ description: "JSON value to canonicalize, or a JSON string to parse first. Objects have keys sorted by UTF-16 code units. Numbers must be integers in ±(2^53−1). Do not pass undefined, functions, or cyclic structures.",
359
+ },
360
+ },
361
+ },
362
+ outputSchema: {
363
+ type: "object",
364
+ additionalProperties: false,
365
+ required: ["canonical"],
366
+ properties: {
367
+ canonical: {
368
+ type: "string",
369
+ description: "RFC 8785 canonical JSON text (UTF-8-ready string). Hash or sign these bytes, not JSON.stringify output.",
370
+ },
371
+ },
372
+ },
373
+ },
374
+ {
375
+ name: "list_scan_rules",
376
+ title: "List the published WARDEN static-scan rule table",
377
+ description: "Return the in-force static-scan ruleset: version, digest, and every rule's code, severity, tier (block vs advise), surfaces (name / description / inputSchema), optional regex source, and named guards. A recorded verdict is only reproducible together with this identity.\n\n" +
378
+ "When to use: explain a finding code, confirm you are on ruleset v4, or re-run a scan with the same table. include_source=true adds the regex source and flags for an independent re-implementation.\n\n" +
379
+ "When NOT to use: evaluating a live tools/list (static_scan_tools or vet_mcp_server — those apply the table). This tool does not scan anything.\n\n" +
380
+ "Behaviour: local snapshot of the compiled rule table, no network, no mutation. Digest is sha256 over the RFC 8785 form of {version, rules}.\n\n" +
381
+ "Returns the ruleset object. Example: list_scan_rules({ include_source: false }).",
382
+ annotations: {
383
+ title: "List the published WARDEN static-scan rule table",
384
+ readOnlyHint: true,
385
+ destructiveHint: false,
386
+ idempotentHint: true,
387
+ openWorldHint: false,
388
+ },
389
+ inputSchema: {
390
+ type: "object",
391
+ additionalProperties: false,
392
+ properties: {
393
+ include_source: {
394
+ type: "boolean",
395
+ default: false,
396
+ description: "When true, each rule includes source (regex body) and flags so a third party can re-run the exact pattern. Default false — identity, tier, surfaces, and guards only, smaller payload.",
397
+ },
398
+ },
399
+ },
400
+ outputSchema: {
401
+ type: "object",
402
+ additionalProperties: false,
403
+ required: ["version", "digest", "rules"],
404
+ properties: {
405
+ version: { type: "string", description: "Monotonic ruleset version." },
406
+ digest: { type: "string", description: "sha256-<base64> of the canonical rule table." },
407
+ rules: {
408
+ type: "array",
409
+ items: {
410
+ type: "object",
411
+ additionalProperties: false,
412
+ required: ["code", "severity", "tier", "surfaces", "guards"],
413
+ properties: {
414
+ code: { type: "string", description: "Finding code this rule emits." },
415
+ severity: { type: "string", enum: SEVERITIES, description: "Attention rank when the rule fires." },
416
+ tier: { type: "string", enum: ["block", "advise"], description: "block can refuse a connection; advise is report-only." },
417
+ surfaces: {
418
+ type: "array",
419
+ items: { type: "string", enum: ["name", "description", "inputSchema"] },
420
+ description: "Which tool-def fields the rule is run against.",
421
+ },
422
+ guards: { type: "array", items: { type: "string" }, description: "Named context guards that can drop a regex match (polarity, mention, …)." },
423
+ source: { type: "string", description: "Regex source; only when include_source is true." },
424
+ flags: { type: "string", description: "Regex flags; only when include_source is true." },
425
+ },
426
+ },
427
+ },
428
+ },
429
+ },
430
+ },
431
+ ];
432
+ export const MCP_INSTRUCTIONS = "WARDEN — MCP security firewall over stdio. Library first; these tools expose the same gates a host would call before any third-party tools/list reaches the model.\n\n" +
433
+ "Tools:\n" +
434
+ "• vet_mcp_server — full chain (static-scan → threat-feed → origin → pinning)\n" +
435
+ "• static_scan_tools — injection/exfil scan only\n" +
436
+ "• classify_sensitive_tools — operator glob split (not an injection scan)\n" +
437
+ "• check_egress_url — hostname allowlist, fail-closed on empty list\n" +
438
+ "• canonicalize_json — RFC 8785 bytes for feeds and pins\n" +
439
+ "• list_scan_rules — published rule table + digest\n\n" +
440
+ "No network, no secrets required. Do not pass live credentials inside tool descriptions you are scanning — they will be echoed in findings. This process does not start other MCP servers.";
441
+ let feedReady;
442
+ async function builtinFeed() {
443
+ if (!feedReady) {
444
+ feedReady = (async () => {
445
+ const feed = new ThreatFeed();
446
+ await feed.load();
447
+ return feed;
448
+ })();
449
+ }
450
+ return feedReady;
451
+ }
452
+ function asTools(raw) {
453
+ if (!Array.isArray(raw) || raw.length === 0) {
454
+ throw new McpToolError("tools must be a non-empty array");
455
+ }
456
+ if (raw.length > MAX_TOOLS) {
457
+ throw new McpToolError(`at most ${MAX_TOOLS} tools per call`);
458
+ }
459
+ return raw.map((item, i) => {
460
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
461
+ throw new McpToolError(`tools[${i}] must be an object`);
462
+ }
463
+ const rec = item;
464
+ const name = rec.name;
465
+ const description = rec.description;
466
+ if (typeof name !== "string" || !name)
467
+ throw new McpToolError(`tools[${i}].name is required`);
468
+ if (typeof description !== "string")
469
+ throw new McpToolError(`tools[${i}].description is required`);
470
+ if (description.length > MAX_STRING)
471
+ throw new McpToolError(`tools[${i}].description exceeds ${MAX_STRING} chars`);
472
+ if (!rec.inputSchema || typeof rec.inputSchema !== "object" || Array.isArray(rec.inputSchema)) {
473
+ throw new McpToolError(`tools[${i}].inputSchema must be a JSON object`);
474
+ }
475
+ return {
476
+ name,
477
+ description,
478
+ inputSchema: rec.inputSchema,
479
+ };
480
+ });
481
+ }
482
+ function asServer(raw) {
483
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
484
+ throw new McpToolError("server must be an object");
485
+ }
486
+ const rec = raw;
487
+ const transport = rec.transport;
488
+ if (transport !== "stdio" && transport !== "sse" && transport !== "http") {
489
+ throw new McpToolError("server.transport must be stdio, sse, or http");
490
+ }
491
+ if (typeof rec.id !== "string" || !rec.id)
492
+ throw new McpToolError("server.id is required");
493
+ if (typeof rec.name !== "string" || !rec.name)
494
+ throw new McpToolError("server.name is required");
495
+ const ref = { id: rec.id, name: rec.name, transport };
496
+ if (typeof rec.command === "string")
497
+ ref.command = rec.command;
498
+ if (Array.isArray(rec.args))
499
+ ref.args = rec.args.map(String);
500
+ if (typeof rec.url === "string")
501
+ ref.url = rec.url;
502
+ if (typeof rec.catalog === "string")
503
+ ref.catalog = rec.catalog;
504
+ return ref;
505
+ }
506
+ function asPolicy(raw) {
507
+ const policy = { ...DEFAULT_MCP_POLICY };
508
+ if (raw == null)
509
+ return policy;
510
+ if (typeof raw !== "object" || Array.isArray(raw))
511
+ throw new McpToolError("policy must be an object");
512
+ const rec = raw;
513
+ if (rec.blockAtSeverity != null) {
514
+ if (!SEVERITIES.includes(rec.blockAtSeverity)) {
515
+ throw new McpToolError(`policy.blockAtSeverity must be one of ${SEVERITIES.join(", ")}`);
516
+ }
517
+ policy.blockAtSeverity = rec.blockAtSeverity;
518
+ }
519
+ if (rec.sensitiveToolPatterns != null) {
520
+ if (!Array.isArray(rec.sensitiveToolPatterns))
521
+ throw new McpToolError("policy.sensitiveToolPatterns must be an array");
522
+ policy.sensitiveToolPatterns = rec.sensitiveToolPatterns.map(String);
523
+ }
524
+ if (typeof rec.allowUnknownServers === "boolean")
525
+ policy.allowUnknownServers = rec.allowUnknownServers;
526
+ if (typeof rec.pinToolDefs === "boolean")
527
+ policy.pinToolDefs = rec.pinToolDefs;
528
+ return policy;
529
+ }
530
+ export class McpToolError extends Error {
531
+ constructor(message) {
532
+ super(message);
533
+ this.name = "McpToolError";
534
+ }
535
+ }
536
+ export async function callMcpTool(name, args) {
537
+ switch (name) {
538
+ case "vet_mcp_server":
539
+ return { structured: { ...(await vetMcpServer(args)) } };
540
+ case "static_scan_tools":
541
+ return { structured: await staticScan(args) };
542
+ case "classify_sensitive_tools":
543
+ return { structured: classify(args) };
544
+ case "check_egress_url":
545
+ return { structured: checkEgress(args) };
546
+ case "canonicalize_json":
547
+ return { structured: canon(args) };
548
+ case "list_scan_rules":
549
+ return { structured: listRules(args) };
550
+ default:
551
+ throw new McpToolError(`unknown tool "${name}"`);
552
+ }
553
+ }
554
+ async function vetMcpServer(args) {
555
+ const tools = asTools(args.tools);
556
+ const server = asServer(args.server);
557
+ const policy = asPolicy(args.policy);
558
+ const threatFeed = await builtinFeed();
559
+ const pins = new Map();
560
+ const warden = Warden.create({
561
+ policy,
562
+ threatFeed,
563
+ store: {
564
+ getPin: async (id) => pins.get(id),
565
+ putPin: async (p) => void pins.set(p.serverId, p),
566
+ },
567
+ });
568
+ return warden.vet(server, tools);
569
+ }
570
+ async function staticScan(args) {
571
+ const tools = asTools(args.tools);
572
+ const gate = new StaticScanGate();
573
+ const result = await gate.evaluate({
574
+ server: { id: "scan@0", name: "scan", transport: "stdio" },
575
+ tools,
576
+ prior: [],
577
+ policy: DEFAULT_MCP_POLICY,
578
+ });
579
+ const ruleset = staticScanRuleset();
580
+ return { score: result.score, findings: result.findings, ruleset: { version: ruleset.version, digest: ruleset.digest } };
581
+ }
582
+ function classify(args) {
583
+ const tools = asTools(args.tools);
584
+ if (!Array.isArray(args.patterns))
585
+ throw new McpToolError("patterns must be an array");
586
+ const patterns = args.patterns.map(String);
587
+ return classifyTools(tools, { ...DEFAULT_MCP_POLICY, sensitiveToolPatterns: patterns });
588
+ }
589
+ function checkEgress(args) {
590
+ if (typeof args.url !== "string" || !args.url)
591
+ throw new McpToolError("url is required");
592
+ if (!Array.isArray(args.allowlist))
593
+ throw new McpToolError("allowlist must be an array");
594
+ const guard = new EgressGuard(args.allowlist.map(String));
595
+ const result = guard.check(args.url);
596
+ let host;
597
+ try {
598
+ host = new URL(args.url).hostname;
599
+ }
600
+ catch {
601
+ host = undefined;
602
+ }
603
+ return result.allowed ? { allowed: true, host } : { allowed: false, host, reason: result.reason };
604
+ }
605
+ function canon(args) {
606
+ let value = args.value;
607
+ if (typeof value === "string") {
608
+ try {
609
+ value = parseJsonStrict(value);
610
+ }
611
+ catch (err) {
612
+ const detail = err instanceof Error ? err.message : String(err);
613
+ throw new McpToolError(`value string is not strict JSON (${detail})`);
614
+ }
615
+ }
616
+ try {
617
+ return { canonical: canonicalize(value) };
618
+ }
619
+ catch (err) {
620
+ if (err instanceof CanonicalizationError)
621
+ throw new McpToolError(`${err.code}: ${err.message}`);
622
+ throw err;
623
+ }
624
+ }
625
+ function listRules(args) {
626
+ const includeSource = args.include_source === true;
627
+ const ruleset = staticScanRuleset();
628
+ const rules = ruleset.rules.map((r) => {
629
+ const row = {
630
+ code: r.code,
631
+ severity: r.severity,
632
+ tier: r.tier,
633
+ surfaces: r.surfaces,
634
+ guards: r.guards,
635
+ };
636
+ if (includeSource) {
637
+ row.source = r.source;
638
+ row.flags = r.flags;
639
+ }
640
+ return row;
641
+ });
642
+ return { version: ruleset.version, digest: ruleset.digest, rules };
643
+ }
644
+ export function payloadTooLarge(args) {
645
+ try {
646
+ return JSON.stringify(args ?? {}).length > MAX_PAYLOAD_CHARS;
647
+ }
648
+ catch {
649
+ return true;
650
+ }
651
+ }
652
+ //# sourceMappingURL=mcp-tools.js.map