synartesis 0.8.8 → 0.9.1
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 +179 -0
- package/README.md +90 -4
- package/dist/{chunk-JE7MOCZO.js → chunk-H4SPMXQW.js} +1875 -73
- package/dist/cli.js +776 -1026
- package/dist/proxy.js +189 -53
- 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 +4 -1
package/dist/proxy.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
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,
|
|
17
|
+
listAll,
|
|
12
18
|
loadManifest,
|
|
13
19
|
mark,
|
|
14
20
|
observeState,
|
|
@@ -18,14 +24,17 @@ import {
|
|
|
18
24
|
qualify,
|
|
19
25
|
refusal,
|
|
20
26
|
runRead,
|
|
27
|
+
startAll,
|
|
21
28
|
toPayload,
|
|
22
|
-
|
|
29
|
+
trustsMarks,
|
|
23
30
|
ungoverned,
|
|
24
31
|
untested,
|
|
32
|
+
upstreamEnv,
|
|
25
33
|
verifyAgainstServers,
|
|
26
34
|
warnUntested,
|
|
27
|
-
withIdempotencyKey
|
|
28
|
-
|
|
35
|
+
withIdempotencyKey,
|
|
36
|
+
withoutMissingTools
|
|
37
|
+
} from "./chunk-H4SPMXQW.js";
|
|
29
38
|
import {
|
|
30
39
|
SnapshotError,
|
|
31
40
|
UpstreamError,
|
|
@@ -147,6 +156,10 @@ async function serveHttp(options) {
|
|
|
147
156
|
await writeResponse(res, await existing.transport.handleRequest(toRequest(req, raw, origin)));
|
|
148
157
|
return;
|
|
149
158
|
}
|
|
159
|
+
if (typeof sessionId === "string") {
|
|
160
|
+
refuse(res, 404, "that session has ended; start a new one with an initialize request");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
150
163
|
const body = req.method === "POST" && raw.length > 0 ? JSON.parse(raw.toString("utf8")) : void 0;
|
|
151
164
|
if (req.method !== "POST" || !isInitialize(body)) {
|
|
152
165
|
refuse(res, 400, "no such session; start one with an initialize request");
|
|
@@ -209,14 +222,28 @@ async function serveHttp(options) {
|
|
|
209
222
|
// src/gate/gate.ts
|
|
210
223
|
var DEFAULT_GATE_TIMEOUT_MS = 3e5;
|
|
211
224
|
var DEFAULT_HINT = (actionId) => `synartesis approve ${actionId.slice(0, 8)}`;
|
|
212
|
-
function createRetryGate(journal, approveHint = DEFAULT_HINT) {
|
|
225
|
+
function createRetryGate(journal, approveHint = DEFAULT_HINT, notify = SILENT) {
|
|
213
226
|
return {
|
|
214
227
|
decide(request) {
|
|
228
|
+
const first = journal.getAction(request.actionId)?.status !== "gated";
|
|
215
229
|
journal.markGated(request.actionId, request.why);
|
|
230
|
+
if (first) {
|
|
231
|
+
notify({
|
|
232
|
+
server: request.server,
|
|
233
|
+
tool: request.tool,
|
|
234
|
+
actionId: request.actionId,
|
|
235
|
+
approve: approveHint(request.actionId)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
216
238
|
return Promise.resolve({
|
|
217
239
|
approved: false,
|
|
218
240
|
awaiting: true,
|
|
219
|
-
|
|
241
|
+
// Nothing here tells the agent how to approve it. This used to hand
|
|
242
|
+
// over the exact `synartesis approve` command to relay, and an agent
|
|
243
|
+
// with a shell -- Claude Code, Codex -- could simply run it, recorded
|
|
244
|
+
// as the person. The command goes to the person instead: in the
|
|
245
|
+
// notification, in `watch`, in the console and in `gates`.
|
|
246
|
+
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
247
|
});
|
|
221
248
|
}
|
|
222
249
|
};
|
|
@@ -285,9 +312,29 @@ function shouldGateOnWrite(args) {
|
|
|
285
312
|
|
|
286
313
|
// src/proxy/proxy.ts
|
|
287
314
|
var APPROVAL_WINDOW_MS = 60 * 60 * 1e3;
|
|
315
|
+
function saidNo(name, denial) {
|
|
316
|
+
return new McpError(
|
|
317
|
+
ErrorCode.InvalidRequest,
|
|
318
|
+
`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.`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
function ago(at) {
|
|
322
|
+
const minutes = Math.max(0, Math.round((Date.now() - Date.parse(at)) / 6e4));
|
|
323
|
+
if (minutes === 0) {
|
|
324
|
+
return "just now";
|
|
325
|
+
}
|
|
326
|
+
return `${String(minutes)} minute${minutes === 1 ? "" : "s"} ago`;
|
|
327
|
+
}
|
|
288
328
|
var PassthroughResult = z.looseObject({});
|
|
289
329
|
var ToolList = z.looseObject({
|
|
290
|
-
tools: z.array(
|
|
330
|
+
tools: z.array(
|
|
331
|
+
z.looseObject({
|
|
332
|
+
name: z.string(),
|
|
333
|
+
// Only the one mark this reads. Anything else a server says passes
|
|
334
|
+
// through untouched, and a malformed value reads as no mark at all.
|
|
335
|
+
annotations: z.looseObject({ readOnlyHint: z.unknown().optional() }).optional().catch(void 0)
|
|
336
|
+
})
|
|
337
|
+
),
|
|
291
338
|
nextCursor: z.string().optional()
|
|
292
339
|
});
|
|
293
340
|
var PromptList = z.looseObject({
|
|
@@ -360,10 +407,11 @@ var SYNARTESIS_INSTRUCTIONS = [
|
|
|
360
407
|
"Some actions cannot be undone. Those are held until a person approves them, and the call",
|
|
361
408
|
'will fail with a message beginning "Synartesis is holding this call for approval".',
|
|
362
409
|
"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
|
|
410
|
+
" 1. Tell the user plainly that the call is waiting for their approval, and what it is for.",
|
|
411
|
+
" They have been notified, and can see and answer it in `synartesis watch`.",
|
|
412
|
+
" 2. Once they say they have approved it, make the same call again. It will go through.",
|
|
413
|
+
"Do not approve it yourself, and do not try to work around a held call by using a",
|
|
414
|
+
"different tool to achieve the same thing."
|
|
367
415
|
].join("\n");
|
|
368
416
|
function instructionsFor(router) {
|
|
369
417
|
const sections = router.upstreams.map((upstream2) => ({
|
|
@@ -422,8 +470,18 @@ function createProxyServer(options) {
|
|
|
422
470
|
const { upstreams, manifest, journal } = options;
|
|
423
471
|
const router = createRouter(upstreams, manifest);
|
|
424
472
|
const policies = createPolicyResolver(manifest);
|
|
473
|
+
const markedReadOnly = /* @__PURE__ */ new Set();
|
|
425
474
|
const log = options.logger;
|
|
426
|
-
const
|
|
475
|
+
const trusting = (found, server2, tool) => {
|
|
476
|
+
if (found.matched || !trustsMarks(manifest, server2) || !markedReadOnly.has(qualify(server2, tool))) {
|
|
477
|
+
return found;
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
policy: { match: qualify(server2, tool), class: "readonly", gate: "never", refusal: "uncertain" },
|
|
481
|
+
matched: false
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
const gate = options.gate ?? createRetryGate(journal, options.approveHint, options.notify);
|
|
427
485
|
const capabilities = mergeCapabilities(
|
|
428
486
|
upstreams.map((upstream) => upstream.client.getServerCapabilities() ?? {})
|
|
429
487
|
);
|
|
@@ -576,6 +634,12 @@ function createProxyServer(options) {
|
|
|
576
634
|
return { items: page.tools, nextCursor: page.nextCursor };
|
|
577
635
|
});
|
|
578
636
|
for (const tool of items) {
|
|
637
|
+
const qualified = qualify(upstream.name, tool.name);
|
|
638
|
+
if (tool.annotations?.readOnlyHint === true) {
|
|
639
|
+
markedReadOnly.add(qualified);
|
|
640
|
+
} else {
|
|
641
|
+
markedReadOnly.delete(qualified);
|
|
642
|
+
}
|
|
579
643
|
tools.push({
|
|
580
644
|
...compatible(tool, (dialect) => {
|
|
581
645
|
log?.warn(
|
|
@@ -602,11 +666,17 @@ function createProxyServer(options) {
|
|
|
602
666
|
`no configured server provides tool ${request.params.name}`
|
|
603
667
|
);
|
|
604
668
|
}
|
|
605
|
-
const { policy } =
|
|
669
|
+
const { policy, matched } = trusting(
|
|
670
|
+
policies.resolve(qualify(route.upstream.name, route.tool)),
|
|
671
|
+
route.upstream.name,
|
|
672
|
+
route.tool
|
|
673
|
+
);
|
|
606
674
|
const args = request.params.arguments ?? {};
|
|
607
675
|
enter();
|
|
608
676
|
try {
|
|
609
|
-
const
|
|
677
|
+
const policyGates = policy.gate === "always" || policy.gate === "on_write" && shouldGateOnWrite(args);
|
|
678
|
+
const allowance = policyGates ? journal.findAllowance(route.upstream.name, route.tool, (/* @__PURE__ */ new Date()).toISOString()) : void 0;
|
|
679
|
+
const wantsGate = policyGates && allowance === void 0;
|
|
610
680
|
const unresolved = policy.class === "readonly" ? void 0 : journal.findPending({
|
|
611
681
|
runId: activeRun,
|
|
612
682
|
server: route.upstream.name,
|
|
@@ -627,6 +697,19 @@ function createProxyServer(options) {
|
|
|
627
697
|
tool: route.tool,
|
|
628
698
|
args
|
|
629
699
|
}) : void 0;
|
|
700
|
+
const refused = mustAsk && granted === void 0 && waiting === void 0 ? journal.findDenial({
|
|
701
|
+
server: route.upstream.name,
|
|
702
|
+
tool: route.tool,
|
|
703
|
+
args,
|
|
704
|
+
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
705
|
+
}) : void 0;
|
|
706
|
+
if (refused !== void 0) {
|
|
707
|
+
log?.info(
|
|
708
|
+
{ action: refused.action.id, by: refused.by },
|
|
709
|
+
"refused again: a person denied this exact call"
|
|
710
|
+
);
|
|
711
|
+
throw saidNo(request.params.name, refused);
|
|
712
|
+
}
|
|
630
713
|
const reusable = waiting ?? (inherited === void 0 ? granted : void 0);
|
|
631
714
|
const pending = reusable === void 0 ? journal.recordPending({
|
|
632
715
|
runId: activeRun,
|
|
@@ -639,6 +722,9 @@ function createProxyServer(options) {
|
|
|
639
722
|
seq: reusable.seq,
|
|
640
723
|
idempotencyKey: reusable.idempotencyKey
|
|
641
724
|
};
|
|
725
|
+
if (allowance !== void 0 && !mustAsk) {
|
|
726
|
+
journal.markAllowed(pending.actionId, `${allowance.by} (allowed without asking)`);
|
|
727
|
+
}
|
|
642
728
|
let spent = true;
|
|
643
729
|
if (inherited !== void 0) {
|
|
644
730
|
spent = journal.adoptApproval(pending.actionId, inherited);
|
|
@@ -709,7 +795,12 @@ function createProxyServer(options) {
|
|
|
709
795
|
if (mustAsk && (granted === void 0 || !spent)) {
|
|
710
796
|
const because = [
|
|
711
797
|
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
|
-
|
|
798
|
+
// Two different problems used to share this one sentence. A tool
|
|
799
|
+
// no rule mentions is held because nobody has said what it does,
|
|
800
|
+
// not because it is known to be permanent -- and the answer to
|
|
801
|
+
// that is a rule, which the person is told how to write, not a
|
|
802
|
+
// yes to every call it makes.
|
|
803
|
+
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
804
|
].filter((one) => one !== void 0);
|
|
714
805
|
await decide(because.join(", and "));
|
|
715
806
|
}
|
|
@@ -764,13 +855,33 @@ function createProxyServer(options) {
|
|
|
764
855
|
}
|
|
765
856
|
const priorState = probe === void 0 || foundNothing ? { present: false } : { present: true, value: snapshot };
|
|
766
857
|
if (noWayBack !== void 0 && !askedAlready) {
|
|
767
|
-
const
|
|
858
|
+
const allowedNow = journal.findAllowance(
|
|
859
|
+
route.upstream.name,
|
|
860
|
+
route.tool,
|
|
861
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
862
|
+
);
|
|
863
|
+
const standing = allowedNow === void 0 ? journal.findApproval({
|
|
768
864
|
server: route.upstream.name,
|
|
769
865
|
tool: route.tool,
|
|
770
866
|
args,
|
|
771
867
|
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
772
|
-
});
|
|
773
|
-
|
|
868
|
+
}) : void 0;
|
|
869
|
+
const refusedHere = allowedNow === void 0 && standing === void 0 ? journal.findDenial({
|
|
870
|
+
server: route.upstream.name,
|
|
871
|
+
tool: route.tool,
|
|
872
|
+
args,
|
|
873
|
+
notBefore: new Date(Date.now() - APPROVAL_WINDOW_MS).toISOString()
|
|
874
|
+
}) : void 0;
|
|
875
|
+
if (allowedNow !== void 0) {
|
|
876
|
+
journal.markAllowed(pending.actionId, `${allowedNow.by} (allowed without asking)`);
|
|
877
|
+
} else if (refusedHere !== void 0) {
|
|
878
|
+
journal.settleAsDenied(
|
|
879
|
+
pending.actionId,
|
|
880
|
+
refusedHere.by,
|
|
881
|
+
`not sent: ${refusedHere.by} had denied this exact call`
|
|
882
|
+
);
|
|
883
|
+
throw saidNo(request.params.name, refusedHere);
|
|
884
|
+
} else if (standing === void 0) {
|
|
774
885
|
await decide(noWayBack.asked);
|
|
775
886
|
} else if (journal.adoptApproval(pending.actionId, standing)) {
|
|
776
887
|
log?.info(
|
|
@@ -794,11 +905,11 @@ function createProxyServer(options) {
|
|
|
794
905
|
signal: extra.signal
|
|
795
906
|
});
|
|
796
907
|
let inferred;
|
|
797
|
-
const
|
|
798
|
-
if (
|
|
908
|
+
const refused2 = refusal(result);
|
|
909
|
+
if (refused2 !== void 0) {
|
|
799
910
|
const settled = policy.refusal === "clean" ? "none" : await whatHappened(router, probe, priorState, extra.signal);
|
|
800
911
|
if (settled !== "applied") {
|
|
801
|
-
const why = `the upstream refused the call: ${
|
|
912
|
+
const why = `the upstream refused the call: ${refused2}`;
|
|
802
913
|
if (settled === "none") {
|
|
803
914
|
journal.markFailed(pending.actionId, why);
|
|
804
915
|
} else {
|
|
@@ -808,14 +919,14 @@ function createProxyServer(options) {
|
|
|
808
919
|
);
|
|
809
920
|
}
|
|
810
921
|
log?.debug(
|
|
811
|
-
{ seq: pending.seq, tool: route.tool, reason:
|
|
922
|
+
{ seq: pending.seq, tool: route.tool, reason: refused2, settled },
|
|
812
923
|
"refused by the upstream"
|
|
813
924
|
);
|
|
814
925
|
return result;
|
|
815
926
|
}
|
|
816
|
-
inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${
|
|
927
|
+
inferred = `the upstream answered with an error and the change was established by reading the resource back, so nothing confirmed it: ${refused2}`;
|
|
817
928
|
log?.warn(
|
|
818
|
-
{ seq: pending.seq, tool: route.tool, reason:
|
|
929
|
+
{ seq: pending.seq, tool: route.tool, reason: refused2 },
|
|
819
930
|
"the upstream reported an error after changing the resource"
|
|
820
931
|
);
|
|
821
932
|
}
|
|
@@ -866,8 +977,9 @@ function createProxyServer(options) {
|
|
|
866
977
|
});
|
|
867
978
|
return result;
|
|
868
979
|
} catch (error) {
|
|
869
|
-
const
|
|
870
|
-
|
|
980
|
+
const kind = route.upstream.classify?.(error);
|
|
981
|
+
const disconnected = isDisconnected(error) || kind === "lost";
|
|
982
|
+
if (neverDispatched(error) || kind !== void 0) {
|
|
871
983
|
journal.markFailed(pending.actionId, describe(error));
|
|
872
984
|
} else {
|
|
873
985
|
const settled = await whatHappened(router, probe, priorState, extra.signal);
|
|
@@ -1188,30 +1300,42 @@ async function main() {
|
|
|
1188
1300
|
`--server ${String(argv.server)} is not declared in ${argv.manifest}; it has: ${declared.map(([name]) => name).join(", ")}`
|
|
1189
1301
|
);
|
|
1190
1302
|
}
|
|
1303
|
+
const source = argv.server === void 0 ? { kind: "manifest" } : { kind: "inherit" };
|
|
1191
1304
|
const upstreams = [];
|
|
1305
|
+
const key = journal.fingerprintKey();
|
|
1306
|
+
const startedWith = /* @__PURE__ */ new Map();
|
|
1307
|
+
upstreams.push(...await startAll([...wanted], ([name, spec]) => connectUpstream(name, spec, { env: source })));
|
|
1192
1308
|
for (const [name, spec] of wanted) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1309
|
+
let client;
|
|
1310
|
+
try {
|
|
1311
|
+
client = source.kind === "inherit" ? clientEnvFor(argv.manifest, name)?.env : void 0;
|
|
1312
|
+
} catch {
|
|
1313
|
+
client = void 0;
|
|
1314
|
+
}
|
|
1315
|
+
startedWith.set(name, {
|
|
1316
|
+
cwd: process.cwd(),
|
|
1317
|
+
fingerprints: fingerprint(key, upstreamEnv(name, spec, source), declaredNames(spec, client))
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
const listed = await listAll(upstreams);
|
|
1321
|
+
const { manifest: served, disabled } = await withoutMissingTools(upstreams, manifest, listed);
|
|
1322
|
+
for (const line of disabled) {
|
|
1323
|
+
log.warn(line);
|
|
1201
1324
|
}
|
|
1202
1325
|
await verifyAgainstServers(
|
|
1203
1326
|
upstreams,
|
|
1204
|
-
argv.server === void 0 ?
|
|
1205
|
-
...
|
|
1206
|
-
tools:
|
|
1207
|
-
}
|
|
1327
|
+
argv.server === void 0 ? served : {
|
|
1328
|
+
...served,
|
|
1329
|
+
tools: served.tools.filter((rule) => rule.match.startsWith(`${String(argv.server)}.`))
|
|
1330
|
+
},
|
|
1331
|
+
listed
|
|
1208
1332
|
);
|
|
1209
1333
|
const uncovered = ungoverned(
|
|
1210
|
-
|
|
1211
|
-
new Map(
|
|
1334
|
+
served,
|
|
1335
|
+
new Map(upstreams.map((upstream) => [
|
|
1212
1336
|
upstream.name,
|
|
1213
|
-
(
|
|
1214
|
-
]))
|
|
1337
|
+
(listed.get(upstream.name) ?? []).filter((tool) => !(tool.readOnly === true && trustsMarks(served, upstream.name))).map((tool) => tool.name)
|
|
1338
|
+
]))
|
|
1215
1339
|
);
|
|
1216
1340
|
for (const entry of uncovered) {
|
|
1217
1341
|
log.warn(
|
|
@@ -1224,21 +1348,33 @@ async function main() {
|
|
|
1224
1348
|
manifest: argv.manifest,
|
|
1225
1349
|
journal: argv.journal,
|
|
1226
1350
|
servers: upstreams.map((upstream) => upstream.name),
|
|
1227
|
-
policies:
|
|
1351
|
+
policies: served.tools.length,
|
|
1352
|
+
held: disabled.length,
|
|
1228
1353
|
ungoverned: uncovered.reduce((sum, entry) => sum + entry.tools.length, 0)
|
|
1229
1354
|
},
|
|
1230
1355
|
"proxy ready"
|
|
1231
1356
|
);
|
|
1232
|
-
const build = () =>
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1357
|
+
const build = () => {
|
|
1358
|
+
const proxy2 = createProxyServer({
|
|
1359
|
+
upstreams,
|
|
1360
|
+
manifest: served,
|
|
1361
|
+
journal,
|
|
1362
|
+
logger: log,
|
|
1363
|
+
// Absolute, because whoever approves may be in any directory at all.
|
|
1364
|
+
approveHint: (actionId) => `${cliCommandFrom(import.meta.url)} approve ${actionId.slice(0, 8)} --journal ${resolve(argv.journal)}`,
|
|
1365
|
+
notify: desktopNotifier()
|
|
1366
|
+
});
|
|
1367
|
+
proxy2.ready.then((runId) => {
|
|
1368
|
+
for (const [server, started] of startedWith) {
|
|
1369
|
+
journal.recordRunServer(runId, server, started.cwd, started.fingerprints);
|
|
1370
|
+
}
|
|
1371
|
+
}).catch((error) => {
|
|
1372
|
+
log.warn({ error: describe(error) }, "could not record what this session's servers were started with");
|
|
1373
|
+
});
|
|
1374
|
+
return proxy2;
|
|
1375
|
+
};
|
|
1240
1376
|
if (argv.http !== void 0) {
|
|
1241
|
-
const
|
|
1377
|
+
const served2 = await serveHttp({
|
|
1242
1378
|
...argv.http,
|
|
1243
1379
|
create: build,
|
|
1244
1380
|
log: {
|
|
@@ -1252,7 +1388,7 @@ async function main() {
|
|
|
1252
1388
|
});
|
|
1253
1389
|
const stop = () => {
|
|
1254
1390
|
void (async () => {
|
|
1255
|
-
await
|
|
1391
|
+
await served2.close();
|
|
1256
1392
|
for (const upstream of upstreams) {
|
|
1257
1393
|
await upstream.close();
|
|
1258
1394
|
}
|
|
@@ -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
|