synartesis 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +78 -4
- package/dist/{chunk-JE7MOCZO.js → chunk-AEEKBR5D.js} +1813 -52
- package/dist/cli.js +710 -1005
- package/dist/proxy.js +182 -49
- package/manifests/aws-docs.yaml +26 -0
- package/manifests/brave.yaml +33 -0
- package/manifests/chrome-devtools.yaml +115 -0
- package/manifests/exa.yaml +20 -0
- package/manifests/fetch.yaml +20 -0
- package/manifests/github.yaml +67 -85
- package/manifests/playwright.yaml +106 -0
- package/manifests/tavily.yaml +27 -0
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
PROXY_FLAGS,
|
|
4
|
+
SILENT,
|
|
4
5
|
canonical,
|
|
5
6
|
cliCommandFrom,
|
|
6
|
-
|
|
7
|
+
clientEnvFor,
|
|
8
|
+
connectUpstream,
|
|
7
9
|
createPolicyResolver,
|
|
8
10
|
createRouter,
|
|
11
|
+
declaredNames,
|
|
12
|
+
desktopNotifier,
|
|
9
13
|
findJournal,
|
|
10
14
|
findManifest,
|
|
15
|
+
fingerprint2 as fingerprint,
|
|
11
16
|
isDisconnected,
|
|
12
17
|
loadManifest,
|
|
13
18
|
mark,
|
|
@@ -20,12 +25,15 @@ import {
|
|
|
20
25
|
runRead,
|
|
21
26
|
toPayload,
|
|
22
27
|
toolShapes,
|
|
28
|
+
trustsMarks,
|
|
23
29
|
ungoverned,
|
|
24
30
|
untested,
|
|
31
|
+
upstreamEnv,
|
|
25
32
|
verifyAgainstServers,
|
|
26
33
|
warnUntested,
|
|
27
|
-
withIdempotencyKey
|
|
28
|
-
|
|
34
|
+
withIdempotencyKey,
|
|
35
|
+
withoutMissingTools
|
|
36
|
+
} from "./chunk-AEEKBR5D.js";
|
|
29
37
|
import {
|
|
30
38
|
SnapshotError,
|
|
31
39
|
UpstreamError,
|
|
@@ -147,6 +155,10 @@ async function serveHttp(options) {
|
|
|
147
155
|
await writeResponse(res, await existing.transport.handleRequest(toRequest(req, raw, origin)));
|
|
148
156
|
return;
|
|
149
157
|
}
|
|
158
|
+
if (typeof sessionId === "string") {
|
|
159
|
+
refuse(res, 404, "that session has ended; start a new one with an initialize request");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
150
162
|
const body = req.method === "POST" && raw.length > 0 ? JSON.parse(raw.toString("utf8")) : void 0;
|
|
151
163
|
if (req.method !== "POST" || !isInitialize(body)) {
|
|
152
164
|
refuse(res, 400, "no such session; start one with an initialize request");
|
|
@@ -209,14 +221,28 @@ async function serveHttp(options) {
|
|
|
209
221
|
// src/gate/gate.ts
|
|
210
222
|
var DEFAULT_GATE_TIMEOUT_MS = 3e5;
|
|
211
223
|
var DEFAULT_HINT = (actionId) => `synartesis approve ${actionId.slice(0, 8)}`;
|
|
212
|
-
function createRetryGate(journal, approveHint = DEFAULT_HINT) {
|
|
224
|
+
function createRetryGate(journal, approveHint = DEFAULT_HINT, notify = SILENT) {
|
|
213
225
|
return {
|
|
214
226
|
decide(request) {
|
|
227
|
+
const first = journal.getAction(request.actionId)?.status !== "gated";
|
|
215
228
|
journal.markGated(request.actionId, request.why);
|
|
229
|
+
if (first) {
|
|
230
|
+
notify({
|
|
231
|
+
server: request.server,
|
|
232
|
+
tool: request.tool,
|
|
233
|
+
actionId: request.actionId,
|
|
234
|
+
approve: approveHint(request.actionId)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
216
237
|
return Promise.resolve({
|
|
217
238
|
approved: false,
|
|
218
239
|
awaiting: true,
|
|
219
|
-
|
|
240
|
+
// Nothing here tells the agent how to approve it. This used to hand
|
|
241
|
+
// over the exact `synartesis approve` command to relay, and an agent
|
|
242
|
+
// with a shell -- Claude Code, Codex -- could simply run it, recorded
|
|
243
|
+
// as the person. The command goes to the person instead: in the
|
|
244
|
+
// notification, in `watch`, in the console and in `gates`.
|
|
245
|
+
reason: "It is waiting for a person to decide, and they have been told. Let the user know it is waiting, then make this exact call again once they say it is approved."
|
|
220
246
|
});
|
|
221
247
|
}
|
|
222
248
|
};
|
|
@@ -285,9 +311,29 @@ function shouldGateOnWrite(args) {
|
|
|
285
311
|
|
|
286
312
|
// src/proxy/proxy.ts
|
|
287
313
|
var APPROVAL_WINDOW_MS = 60 * 60 * 1e3;
|
|
314
|
+
function saidNo(name, denial) {
|
|
315
|
+
return new McpError(
|
|
316
|
+
ErrorCode.InvalidRequest,
|
|
317
|
+
`synartesis blocked ${name}: ${denial.by} denied this exact call ${ago(denial.at)} (${denial.reason}). Ask the user what they want instead; do not retry it or reach the same result another way.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
function ago(at) {
|
|
321
|
+
const minutes = Math.max(0, Math.round((Date.now() - Date.parse(at)) / 6e4));
|
|
322
|
+
if (minutes === 0) {
|
|
323
|
+
return "just now";
|
|
324
|
+
}
|
|
325
|
+
return `${String(minutes)} minute${minutes === 1 ? "" : "s"} ago`;
|
|
326
|
+
}
|
|
288
327
|
var PassthroughResult = z.looseObject({});
|
|
289
328
|
var ToolList = z.looseObject({
|
|
290
|
-
tools: z.array(
|
|
329
|
+
tools: z.array(
|
|
330
|
+
z.looseObject({
|
|
331
|
+
name: z.string(),
|
|
332
|
+
// Only the one mark this reads. Anything else a server says passes
|
|
333
|
+
// through untouched, and a malformed value reads as no mark at all.
|
|
334
|
+
annotations: z.looseObject({ readOnlyHint: z.unknown().optional() }).optional().catch(void 0)
|
|
335
|
+
})
|
|
336
|
+
),
|
|
291
337
|
nextCursor: z.string().optional()
|
|
292
338
|
});
|
|
293
339
|
var PromptList = z.looseObject({
|
|
@@ -360,10 +406,11 @@ var SYNARTESIS_INSTRUCTIONS = [
|
|
|
360
406
|
"Some actions cannot be undone. Those are held until a person approves them, and the call",
|
|
361
407
|
'will fail with a message beginning "Synartesis is holding this call for approval".',
|
|
362
408
|
"When that happens:",
|
|
363
|
-
" 1. Tell the user plainly that
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"Do not try to work around a held call by using a
|
|
409
|
+
" 1. Tell the user plainly that the call is waiting for their approval, and what it is for.",
|
|
410
|
+
" They have been notified, and can see and answer it in `synartesis watch`.",
|
|
411
|
+
" 2. Once they say they have approved it, make the same call again. It will go through.",
|
|
412
|
+
"Do not approve it yourself, and do not try to work around a held call by using a",
|
|
413
|
+
"different tool to achieve the same thing."
|
|
367
414
|
].join("\n");
|
|
368
415
|
function instructionsFor(router) {
|
|
369
416
|
const sections = router.upstreams.map((upstream2) => ({
|
|
@@ -422,8 +469,18 @@ function createProxyServer(options) {
|
|
|
422
469
|
const { upstreams, manifest, journal } = options;
|
|
423
470
|
const router = createRouter(upstreams, manifest);
|
|
424
471
|
const policies = createPolicyResolver(manifest);
|
|
472
|
+
const markedReadOnly = /* @__PURE__ */ new Set();
|
|
425
473
|
const log = options.logger;
|
|
426
|
-
const
|
|
474
|
+
const trusting = (found, server2, tool) => {
|
|
475
|
+
if (found.matched || !trustsMarks(manifest, server2) || !markedReadOnly.has(qualify(server2, tool))) {
|
|
476
|
+
return found;
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
policy: { match: qualify(server2, tool), class: "readonly", gate: "never", refusal: "uncertain" },
|
|
480
|
+
matched: false
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
const gate = options.gate ?? createRetryGate(journal, options.approveHint, options.notify);
|
|
427
484
|
const capabilities = mergeCapabilities(
|
|
428
485
|
upstreams.map((upstream) => upstream.client.getServerCapabilities() ?? {})
|
|
429
486
|
);
|
|
@@ -576,6 +633,12 @@ function createProxyServer(options) {
|
|
|
576
633
|
return { items: page.tools, nextCursor: page.nextCursor };
|
|
577
634
|
});
|
|
578
635
|
for (const tool of items) {
|
|
636
|
+
const qualified = qualify(upstream.name, tool.name);
|
|
637
|
+
if (tool.annotations?.readOnlyHint === true) {
|
|
638
|
+
markedReadOnly.add(qualified);
|
|
639
|
+
} else {
|
|
640
|
+
markedReadOnly.delete(qualified);
|
|
641
|
+
}
|
|
579
642
|
tools.push({
|
|
580
643
|
...compatible(tool, (dialect) => {
|
|
581
644
|
log?.warn(
|
|
@@ -602,11 +665,17 @@ function createProxyServer(options) {
|
|
|
602
665
|
`no configured server provides tool ${request.params.name}`
|
|
603
666
|
);
|
|
604
667
|
}
|
|
605
|
-
const { policy } =
|
|
668
|
+
const { policy, matched } = trusting(
|
|
669
|
+
policies.resolve(qualify(route.upstream.name, route.tool)),
|
|
670
|
+
route.upstream.name,
|
|
671
|
+
route.tool
|
|
672
|
+
);
|
|
606
673
|
const args = request.params.arguments ?? {};
|
|
607
674
|
enter();
|
|
608
675
|
try {
|
|
609
|
-
const
|
|
676
|
+
const policyGates = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
|
|
677
|
+
const allowance = policyGates ? journal.findAllowance(route.upstream.name, route.tool, (/* @__PURE__ */ new Date()).toISOString()) : void 0;
|
|
678
|
+
const wantsGate = policyGates && allowance === void 0;
|
|
610
679
|
const unresolved = policy.class === "readonly" ? void 0 : journal.findPending({
|
|
611
680
|
runId: activeRun,
|
|
612
681
|
server: route.upstream.name,
|
|
@@ -627,6 +696,19 @@ function createProxyServer(options) {
|
|
|
627
696
|
tool: route.tool,
|
|
628
697
|
args
|
|
629
698
|
}) : void 0;
|
|
699
|
+
const refused = mustAsk && granted === void 0 && waiting === void 0 ? journal.findDenial({
|
|
700
|
+
server: route.upstream.name,
|
|
701
|
+
tool: route.tool,
|
|
702
|
+
args,
|
|
703
|
+
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
704
|
+
}) : void 0;
|
|
705
|
+
if (refused !== void 0) {
|
|
706
|
+
log?.info(
|
|
707
|
+
{ action: refused.action.id, by: refused.by },
|
|
708
|
+
"refused again: a person denied this exact call"
|
|
709
|
+
);
|
|
710
|
+
throw saidNo(request.params.name, refused);
|
|
711
|
+
}
|
|
630
712
|
const reusable = waiting ?? (inherited === void 0 ? granted : void 0);
|
|
631
713
|
const pending = reusable === void 0 ? journal.recordPending({
|
|
632
714
|
runId: activeRun,
|
|
@@ -639,6 +721,9 @@ function createProxyServer(options) {
|
|
|
639
721
|
seq: reusable.seq,
|
|
640
722
|
idempotencyKey: reusable.idempotencyKey
|
|
641
723
|
};
|
|
724
|
+
if (allowance !== void 0 && !mustAsk) {
|
|
725
|
+
journal.markAllowed(pending.actionId, `${allowance.by} (allowed without asking)`);
|
|
726
|
+
}
|
|
642
727
|
let spent = true;
|
|
643
728
|
if (inherited !== void 0) {
|
|
644
729
|
spent = journal.adoptApproval(pending.actionId, inherited);
|
|
@@ -709,7 +794,12 @@ function createProxyServer(options) {
|
|
|
709
794
|
if (mustAsk && (granted === void 0 || !spent)) {
|
|
710
795
|
const because = [
|
|
711
796
|
unresolved === void 0 ? void 0 : `an earlier attempt at this exact call, action ${String(unresolved.seq)}, went out and its outcome could never be established, so sending it again may apply the change a second time`,
|
|
712
|
-
|
|
797
|
+
// Two different problems used to share this one sentence. A tool
|
|
798
|
+
// no rule mentions is held because nobody has said what it does,
|
|
799
|
+
// not because it is known to be permanent -- and the answer to
|
|
800
|
+
// that is a rule, which the person is told how to write, not a
|
|
801
|
+
// yes to every call it makes.
|
|
802
|
+
wantsGate ? matched ? "this action cannot be undone" : `there is no rule for ${route.upstream.name}.${route.tool} in the policy, so nothing says whether it can be undone` : void 0
|
|
713
803
|
].filter((one) => one !== void 0);
|
|
714
804
|
await decide(because.join(", and "));
|
|
715
805
|
}
|
|
@@ -764,13 +854,33 @@ function createProxyServer(options) {
|
|
|
764
854
|
}
|
|
765
855
|
const priorState = probe === void 0 || foundNothing ? { present: false } : { present: true, value: snapshot };
|
|
766
856
|
if (noWayBack !== void 0 && !askedAlready) {
|
|
767
|
-
const
|
|
857
|
+
const allowedNow = journal.findAllowance(
|
|
858
|
+
route.upstream.name,
|
|
859
|
+
route.tool,
|
|
860
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
861
|
+
);
|
|
862
|
+
const standing = allowedNow === void 0 ? journal.findApproval({
|
|
768
863
|
server: route.upstream.name,
|
|
769
864
|
tool: route.tool,
|
|
770
865
|
args,
|
|
771
866
|
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
772
|
-
});
|
|
773
|
-
|
|
867
|
+
}) : void 0;
|
|
868
|
+
const refusedHere = allowedNow === void 0 && standing === void 0 ? journal.findDenial({
|
|
869
|
+
server: route.upstream.name,
|
|
870
|
+
tool: route.tool,
|
|
871
|
+
args,
|
|
872
|
+
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
873
|
+
}) : void 0;
|
|
874
|
+
if (allowedNow !== void 0) {
|
|
875
|
+
journal.markAllowed(pending.actionId, `${allowedNow.by} (allowed without asking)`);
|
|
876
|
+
} else if (refusedHere !== void 0) {
|
|
877
|
+
journal.settleAsDenied(
|
|
878
|
+
pending.actionId,
|
|
879
|
+
refusedHere.by,
|
|
880
|
+
`not sent: ${refusedHere.by} had denied this exact call`
|
|
881
|
+
);
|
|
882
|
+
throw saidNo(request.params.name, refusedHere);
|
|
883
|
+
} else if (standing === void 0) {
|
|
774
884
|
await decide(noWayBack.asked);
|
|
775
885
|
} else if (journal.adoptApproval(pending.actionId, standing)) {
|
|
776
886
|
log?.info(
|
|
@@ -794,11 +904,11 @@ function createProxyServer(options) {
|
|
|
794
904
|
signal: extra.signal
|
|
795
905
|
});
|
|
796
906
|
let inferred;
|
|
797
|
-
const
|
|
798
|
-
if (
|
|
907
|
+
const refused2 = refusal(result);
|
|
908
|
+
if (refused2 !== void 0) {
|
|
799
909
|
const settled = policy.refusal === "clean" ? "none" : await whatHappened(router, probe, priorState, extra.signal);
|
|
800
910
|
if (settled !== "applied") {
|
|
801
|
-
const why = `the upstream refused the call: ${
|
|
911
|
+
const why = `the upstream refused the call: ${refused2}`;
|
|
802
912
|
if (settled === "none") {
|
|
803
913
|
journal.markFailed(pending.actionId, why);
|
|
804
914
|
} else {
|
|
@@ -808,14 +918,14 @@ function createProxyServer(options) {
|
|
|
808
918
|
);
|
|
809
919
|
}
|
|
810
920
|
log?.debug(
|
|
811
|
-
{ seq: pending.seq, tool: route.tool, reason:
|
|
921
|
+
{ seq: pending.seq, tool: route.tool, reason: refused2, settled },
|
|
812
922
|
"refused by the upstream"
|
|
813
923
|
);
|
|
814
924
|
return result;
|
|
815
925
|
}
|
|
816
|
-
inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${
|
|
926
|
+
inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${refused2}`;
|
|
817
927
|
log?.warn(
|
|
818
|
-
{ seq: pending.seq, tool: route.tool, reason:
|
|
928
|
+
{ seq: pending.seq, tool: route.tool, reason: refused2 },
|
|
819
929
|
"the upstream reported an error after changing the resource"
|
|
820
930
|
);
|
|
821
931
|
}
|
|
@@ -866,8 +976,9 @@ function createProxyServer(options) {
|
|
|
866
976
|
});
|
|
867
977
|
return result;
|
|
868
978
|
} catch (error) {
|
|
869
|
-
const
|
|
870
|
-
|
|
979
|
+
const kind = route.upstream.classify?.(error);
|
|
980
|
+
const disconnected = isDisconnected(error) || kind === "lost";
|
|
981
|
+
if (neverDispatched(error) || kind !== void 0) {
|
|
871
982
|
journal.markFailed(pending.actionId, describe(error));
|
|
872
983
|
} else {
|
|
873
984
|
const settled = await whatHappened(router, probe, priorState, extra.signal);
|
|
@@ -1188,29 +1299,39 @@ async function main() {
|
|
|
1188
1299
|
`--server ${String(argv.server)} is not declared in ${argv.manifest}; it has: ${declared.map(([name]) => name).join(", ")}`
|
|
1189
1300
|
);
|
|
1190
1301
|
}
|
|
1302
|
+
const source = argv.server === void 0 ? { kind: "manifest" } : { kind: "inherit" };
|
|
1191
1303
|
const upstreams = [];
|
|
1304
|
+
const key = journal.fingerprintKey();
|
|
1305
|
+
const startedWith = /* @__PURE__ */ new Map();
|
|
1192
1306
|
for (const [name, spec] of wanted) {
|
|
1193
|
-
upstreams.push(
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1307
|
+
upstreams.push(await connectUpstream(name, spec, { env: source }));
|
|
1308
|
+
let client;
|
|
1309
|
+
try {
|
|
1310
|
+
client = source.kind === "inherit" ? clientEnvFor(argv.manifest, name)?.env : void 0;
|
|
1311
|
+
} catch {
|
|
1312
|
+
client = void 0;
|
|
1313
|
+
}
|
|
1314
|
+
startedWith.set(name, {
|
|
1315
|
+
cwd: process.cwd(),
|
|
1316
|
+
fingerprints: fingerprint(key, upstreamEnv(name, spec, source), declaredNames(spec, client))
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
const { manifest: served, disabled } = await withoutMissingTools(upstreams, manifest);
|
|
1320
|
+
for (const line of disabled) {
|
|
1321
|
+
log.warn(line);
|
|
1201
1322
|
}
|
|
1202
1323
|
await verifyAgainstServers(
|
|
1203
1324
|
upstreams,
|
|
1204
|
-
argv.server === void 0 ?
|
|
1205
|
-
...
|
|
1206
|
-
tools:
|
|
1325
|
+
argv.server === void 0 ? served : {
|
|
1326
|
+
...served,
|
|
1327
|
+
tools: served.tools.filter((rule) => rule.match.startsWith(`${String(argv.server)}.`))
|
|
1207
1328
|
}
|
|
1208
1329
|
);
|
|
1209
1330
|
const uncovered = ungoverned(
|
|
1210
|
-
|
|
1331
|
+
served,
|
|
1211
1332
|
new Map(await Promise.all(upstreams.map(async (upstream) => [
|
|
1212
1333
|
upstream.name,
|
|
1213
|
-
(await toolShapes(upstream)).map((tool) => tool.name)
|
|
1334
|
+
(await toolShapes(upstream)).filter((tool) => !(tool.readOnly === true && trustsMarks(served, upstream.name))).map((tool) => tool.name)
|
|
1214
1335
|
])))
|
|
1215
1336
|
);
|
|
1216
1337
|
for (const entry of uncovered) {
|
|
@@ -1224,21 +1345,33 @@ async function main() {
|
|
|
1224
1345
|
manifest: argv.manifest,
|
|
1225
1346
|
journal: argv.journal,
|
|
1226
1347
|
servers: upstreams.map((upstream) => upstream.name),
|
|
1227
|
-
policies:
|
|
1348
|
+
policies: served.tools.length,
|
|
1349
|
+
held: disabled.length,
|
|
1228
1350
|
ungoverned: uncovered.reduce((sum, entry) => sum + entry.tools.length, 0)
|
|
1229
1351
|
},
|
|
1230
1352
|
"proxy ready"
|
|
1231
1353
|
);
|
|
1232
|
-
const build = () =>
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1354
|
+
const build = () => {
|
|
1355
|
+
const proxy2 = createProxyServer({
|
|
1356
|
+
upstreams,
|
|
1357
|
+
manifest: served,
|
|
1358
|
+
journal,
|
|
1359
|
+
logger: log,
|
|
1360
|
+
// Absolute, because whoever approves may be in any directory at all.
|
|
1361
|
+
approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`,
|
|
1362
|
+
notify: desktopNotifier()
|
|
1363
|
+
});
|
|
1364
|
+
proxy2.ready.then((runId) => {
|
|
1365
|
+
for (const [server, started] of startedWith) {
|
|
1366
|
+
journal.recordRunServer(runId, server, started.cwd, started.fingerprints);
|
|
1367
|
+
}
|
|
1368
|
+
}).catch((error) => {
|
|
1369
|
+
log.warn({ error: describe(error) }, "could not record what this session's servers were started with");
|
|
1370
|
+
});
|
|
1371
|
+
return proxy2;
|
|
1372
|
+
};
|
|
1240
1373
|
if (argv.http !== void 0) {
|
|
1241
|
-
const
|
|
1374
|
+
const served2 = await serveHttp({
|
|
1242
1375
|
...argv.http,
|
|
1243
1376
|
create: build,
|
|
1244
1377
|
log: {
|
|
@@ -1252,7 +1385,7 @@ async function main() {
|
|
|
1252
1385
|
});
|
|
1253
1386
|
const stop = () => {
|
|
1254
1387
|
void (async () => {
|
|
1255
|
-
await
|
|
1388
|
+
await served2.close();
|
|
1256
1389
|
for (const upstream of upstreams) {
|
|
1257
1390
|
await upstream.close();
|
|
1258
1391
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Policy for awslabs.aws-documentation-mcp-server.
|
|
2
|
+
#
|
|
3
|
+
# Written against the build current in September 2026 from the server's own tools/list.
|
|
4
|
+
#
|
|
5
|
+
# Reads AWS's public documentation. Nothing here touches an AWS account.
|
|
6
|
+
#
|
|
7
|
+
# A tool a later version adds is held until a rule says what it does.
|
|
8
|
+
version: 1
|
|
9
|
+
|
|
10
|
+
servers:
|
|
11
|
+
aws-docs:
|
|
12
|
+
command: uvx
|
|
13
|
+
args: ["awslabs.aws-documentation-mcp-server@latest"]
|
|
14
|
+
provenance: live
|
|
15
|
+
|
|
16
|
+
tools:
|
|
17
|
+
- match: "aws-docs.read_documentation"
|
|
18
|
+
class: readonly
|
|
19
|
+
- match: "aws-docs.read_sections"
|
|
20
|
+
class: readonly
|
|
21
|
+
- match: "aws-docs.search_table"
|
|
22
|
+
class: readonly
|
|
23
|
+
- match: "aws-docs.search_documentation"
|
|
24
|
+
class: readonly
|
|
25
|
+
- match: "aws-docs.recommend"
|
|
26
|
+
class: readonly
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Policy for @brave/brave-search-mcp-server.
|
|
2
|
+
#
|
|
3
|
+
# Written against 2.1.4 from the server's own tools/list.
|
|
4
|
+
#
|
|
5
|
+
# Every tool searches. The server does not mark them read-only, which is why
|
|
6
|
+
# each search was held before this file existed.
|
|
7
|
+
#
|
|
8
|
+
# A tool a later version adds is held until a rule says what it does.
|
|
9
|
+
version: 1
|
|
10
|
+
|
|
11
|
+
servers:
|
|
12
|
+
brave:
|
|
13
|
+
command: npx
|
|
14
|
+
args: ["-y", "@brave/brave-search-mcp-server"]
|
|
15
|
+
provenance: live
|
|
16
|
+
|
|
17
|
+
tools:
|
|
18
|
+
- match: "brave.brave_web_search"
|
|
19
|
+
class: readonly
|
|
20
|
+
- match: "brave.brave_local_search"
|
|
21
|
+
class: readonly
|
|
22
|
+
- match: "brave.brave_video_search"
|
|
23
|
+
class: readonly
|
|
24
|
+
- match: "brave.brave_image_search"
|
|
25
|
+
class: readonly
|
|
26
|
+
- match: "brave.brave_news_search"
|
|
27
|
+
class: readonly
|
|
28
|
+
- match: "brave.brave_summarizer"
|
|
29
|
+
class: readonly
|
|
30
|
+
- match: "brave.brave_llm_context"
|
|
31
|
+
class: readonly
|
|
32
|
+
- match: "brave.brave_place_search"
|
|
33
|
+
class: readonly
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Policy for chrome-devtools-mcp.
|
|
2
|
+
#
|
|
3
|
+
# Written against 1.10.1 from the server's own tools/list, read-only hints and
|
|
4
|
+
# all.
|
|
5
|
+
#
|
|
6
|
+
# What playwright.yaml says holds here too. Nothing a browser does can be
|
|
7
|
+
# undone, and holding every click made the server unusable, so interactions
|
|
8
|
+
# are recorded rather than held: each is classed as a call that cannot be
|
|
9
|
+
# undone, and written to the journal with its arguments.
|
|
10
|
+
#
|
|
11
|
+
# Two tools are still held. Running a script the agent wrote has the reach of
|
|
12
|
+
# the page and everything it is signed in to, and an upload sends a file from
|
|
13
|
+
# your disk to whoever runs the page. A tool this file does not mention is held
|
|
14
|
+
# until a rule says otherwise.
|
|
15
|
+
#
|
|
16
|
+
# Only the tools the server marks read-only are classed as reads. The ones it
|
|
17
|
+
# does not include screenshots, snapshots, traces and audits, and several of
|
|
18
|
+
# those can write a file to disk when given a path. They are recorded, not
|
|
19
|
+
# held.
|
|
20
|
+
version: 1
|
|
21
|
+
|
|
22
|
+
servers:
|
|
23
|
+
chrome-devtools:
|
|
24
|
+
command: npx
|
|
25
|
+
args: ["-y", "chrome-devtools-mcp@latest"]
|
|
26
|
+
provenance: live
|
|
27
|
+
|
|
28
|
+
tools:
|
|
29
|
+
# --- reads: the tools the server itself marks read-only ------------------
|
|
30
|
+
- match: "chrome-devtools.list_pages"
|
|
31
|
+
class: readonly
|
|
32
|
+
- match: "chrome-devtools.select_page"
|
|
33
|
+
class: readonly
|
|
34
|
+
- match: "chrome-devtools.list_console_messages"
|
|
35
|
+
class: readonly
|
|
36
|
+
- match: "chrome-devtools.get_console_message"
|
|
37
|
+
class: readonly
|
|
38
|
+
- match: "chrome-devtools.list_network_requests"
|
|
39
|
+
class: readonly
|
|
40
|
+
- match: "chrome-devtools.get_css_styles"
|
|
41
|
+
class: readonly
|
|
42
|
+
- match: "chrome-devtools.performance_analyze_insight"
|
|
43
|
+
class: readonly
|
|
44
|
+
- match: "chrome-devtools.wait_for"
|
|
45
|
+
class: readonly
|
|
46
|
+
|
|
47
|
+
# --- held: each reaches past the page -------------------------------------
|
|
48
|
+
- match: "chrome-devtools.evaluate_script"
|
|
49
|
+
class: irreversible
|
|
50
|
+
gate: always
|
|
51
|
+
- match: "chrome-devtools.upload_file"
|
|
52
|
+
class: irreversible
|
|
53
|
+
gate: always
|
|
54
|
+
|
|
55
|
+
# --- recorded, not held ----------------------------------------------------
|
|
56
|
+
- match: "chrome-devtools.navigate_page"
|
|
57
|
+
class: irreversible
|
|
58
|
+
gate: never
|
|
59
|
+
- match: "chrome-devtools.new_page"
|
|
60
|
+
class: irreversible
|
|
61
|
+
gate: never
|
|
62
|
+
- match: "chrome-devtools.close_page"
|
|
63
|
+
class: irreversible
|
|
64
|
+
gate: never
|
|
65
|
+
- match: "chrome-devtools.click"
|
|
66
|
+
class: irreversible
|
|
67
|
+
gate: never
|
|
68
|
+
- match: "chrome-devtools.fill"
|
|
69
|
+
class: irreversible
|
|
70
|
+
gate: never
|
|
71
|
+
- match: "chrome-devtools.fill_form"
|
|
72
|
+
class: irreversible
|
|
73
|
+
gate: never
|
|
74
|
+
- match: "chrome-devtools.type_text"
|
|
75
|
+
class: irreversible
|
|
76
|
+
gate: never
|
|
77
|
+
- match: "chrome-devtools.press_key"
|
|
78
|
+
class: irreversible
|
|
79
|
+
gate: never
|
|
80
|
+
- match: "chrome-devtools.hover"
|
|
81
|
+
class: irreversible
|
|
82
|
+
gate: never
|
|
83
|
+
- match: "chrome-devtools.drag"
|
|
84
|
+
class: irreversible
|
|
85
|
+
gate: never
|
|
86
|
+
- match: "chrome-devtools.handle_dialog"
|
|
87
|
+
class: irreversible
|
|
88
|
+
gate: never
|
|
89
|
+
- match: "chrome-devtools.emulate"
|
|
90
|
+
class: irreversible
|
|
91
|
+
gate: never
|
|
92
|
+
- match: "chrome-devtools.resize_page"
|
|
93
|
+
class: irreversible
|
|
94
|
+
gate: never
|
|
95
|
+
- match: "chrome-devtools.take_screenshot"
|
|
96
|
+
class: irreversible
|
|
97
|
+
gate: never
|
|
98
|
+
- match: "chrome-devtools.take_snapshot"
|
|
99
|
+
class: irreversible
|
|
100
|
+
gate: never
|
|
101
|
+
- match: "chrome-devtools.take_heapsnapshot"
|
|
102
|
+
class: irreversible
|
|
103
|
+
gate: never
|
|
104
|
+
- match: "chrome-devtools.get_network_request"
|
|
105
|
+
class: irreversible
|
|
106
|
+
gate: never
|
|
107
|
+
- match: "chrome-devtools.performance_start_trace"
|
|
108
|
+
class: irreversible
|
|
109
|
+
gate: never
|
|
110
|
+
- match: "chrome-devtools.performance_stop_trace"
|
|
111
|
+
class: irreversible
|
|
112
|
+
gate: never
|
|
113
|
+
- match: "chrome-devtools.lighthouse_audit"
|
|
114
|
+
class: irreversible
|
|
115
|
+
gate: never
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Policy for exa-mcp-server.
|
|
2
|
+
#
|
|
3
|
+
# Written against 3.4.1 from the server's own tools/list.
|
|
4
|
+
#
|
|
5
|
+
# Both tools are marked read-only by the server itself.
|
|
6
|
+
#
|
|
7
|
+
# A tool a later version adds is held until a rule says what it does.
|
|
8
|
+
version: 1
|
|
9
|
+
|
|
10
|
+
servers:
|
|
11
|
+
exa:
|
|
12
|
+
command: npx
|
|
13
|
+
args: ["-y", "exa-mcp-server"]
|
|
14
|
+
provenance: live
|
|
15
|
+
|
|
16
|
+
tools:
|
|
17
|
+
- match: "exa.web_search_exa"
|
|
18
|
+
class: readonly
|
|
19
|
+
- match: "exa.web_fetch_exa"
|
|
20
|
+
class: readonly
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Policy for mcp-server-fetch.
|
|
2
|
+
#
|
|
3
|
+
# Written against 1.30.0 from the server's own tools/list.
|
|
4
|
+
#
|
|
5
|
+
# It fetches a URL and hands back what is there. A GET can still reach
|
|
6
|
+
# something that changes state on being read -- that is the page's doing, and
|
|
7
|
+
# no policy here can see it.
|
|
8
|
+
#
|
|
9
|
+
# A tool a later version adds is held until a rule says what it does.
|
|
10
|
+
version: 1
|
|
11
|
+
|
|
12
|
+
servers:
|
|
13
|
+
fetch:
|
|
14
|
+
command: uvx
|
|
15
|
+
args: ["mcp-server-fetch"]
|
|
16
|
+
provenance: live
|
|
17
|
+
|
|
18
|
+
tools:
|
|
19
|
+
- match: "fetch.fetch"
|
|
20
|
+
class: readonly
|