mcp-authz 0.4.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.
- package/README.md +69 -20
- package/dist/cli.js +340 -106
- package/dist/definitions-CZVk-j1C.d.ts +5 -0
- package/dist/definitions-CyIy4YSZ.js +132 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +16 -7
- package/dist/{ladder-CUzOKudC.js → ladder-6TnD3hTJ.js} +3 -1
- package/dist/openapi.d.ts +2 -2
- package/dist/{permissions-module-Cr0K1a7x.d.ts → permissions-module-TOpt20D4.d.ts} +2 -2
- package/dist/proxy.d.ts +13 -0
- package/dist/proxy.js +287 -34
- package/dist/screen-DxoujEpO.js +79 -0
- package/dist/strict-json-DLKOgsGE.js +100 -0
- package/dist/testing.d.ts +8 -3
- package/dist/testing.js +54 -62
- package/dist/{tools-BQE1O-7P.d.ts → tools-C1dZESYK.d.ts} +9 -2
- package/package.json +1 -1
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { a as listCatalogue, i as definitionOf, t as INSTRUCTIONS } from "./definitions-CyIy4YSZ.js";
|
|
1
2
|
import { InMemoryTransport } from "@modelcontextprotocol/server";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
3
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
4
4
|
//#region src/permissions-module.ts
|
|
5
5
|
/**
|
|
@@ -22,29 +22,44 @@ function toPermissionsModule(record) {
|
|
|
22
22
|
"} as const;",
|
|
23
23
|
"",
|
|
24
24
|
"export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];",
|
|
25
|
-
...
|
|
25
|
+
...definitionLines(record),
|
|
26
26
|
...resourceUriLines(record.resourceUris ?? {}),
|
|
27
27
|
""
|
|
28
28
|
].join("\n");
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Only emitted when the recorder could
|
|
31
|
+
* Only emitted when the recorder could read definitions. An OpenAPI document is
|
|
32
32
|
* already a file in the repository, diffed on the pull request by whoever
|
|
33
|
-
* changed it, so there is nothing for a second
|
|
33
|
+
* changed it, so there is nothing for a second record to catch.
|
|
34
|
+
*
|
|
35
|
+
* Written out in full rather than as digests, so re-recording shows in review
|
|
36
|
+
* the exact words a server changed, and so `createMcpProxy` can compare without
|
|
37
|
+
* hashing on every listing.
|
|
34
38
|
*/
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
if (Object.keys(
|
|
39
|
+
function definitionLines(record) {
|
|
40
|
+
const definitions = record.definitions ?? {};
|
|
41
|
+
if (Object.keys(definitions).length === 0) return [];
|
|
38
42
|
return [
|
|
39
43
|
"",
|
|
40
|
-
"// What each capability
|
|
41
|
-
"//
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
"// What each capability said to the model when this was recorded. createMcpProxy()",
|
|
45
|
+
"// hides one that no longer matches, so a server cannot keep an approved name and",
|
|
46
|
+
"// change what it tells the model. Re-record to approve a change.",
|
|
47
|
+
"export const DEFINITIONS = {",
|
|
48
|
+
...record.names.map((label) => ` ${quote(label)}: ${value(definitions[label] ?? {})},`),
|
|
49
|
+
...definitions["server:instructions"] ? [` ${quote(INSTRUCTIONS)}: ${value(definitions[INSTRUCTIONS])},`] : [],
|
|
50
|
+
"};"
|
|
45
51
|
];
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
54
|
+
* JSON is a valid object literal except for one key: `"__proto__"` sets the
|
|
55
|
+
* prototype there, dropping that part of a schema. A definition carrying one is
|
|
56
|
+
* written as a parse of its JSON instead, which keeps it.
|
|
57
|
+
*/
|
|
58
|
+
function value(definition) {
|
|
59
|
+
const json = JSON.stringify(definition, null, 2).replace(/\n/g, "\n ");
|
|
60
|
+
return json.includes("\"__proto__\"") ? `JSON.parse(${literal(JSON.stringify(definition))})` : json;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
48
63
|
* Only emitted when the server has resources, so a tools-only map stays a map.
|
|
49
64
|
* `createMcpProxy` needs this to price a read, which names a URI and never a
|
|
50
65
|
* label; `gate()` never sees it.
|
|
@@ -85,19 +100,14 @@ function literal(value) {
|
|
|
85
100
|
}
|
|
86
101
|
//#endregion
|
|
87
102
|
//#region src/testing.ts
|
|
88
|
-
function digest(parts) {
|
|
89
|
-
return createHash("sha256").update(JSON.stringify(canonical(parts))).digest("hex").slice(0, 16);
|
|
90
|
-
}
|
|
91
103
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
104
|
+
* An upstream is recorded in 2026-07-28 and nothing older: the proxy that
|
|
105
|
+
* enforces the record speaks only that, and a record read through another
|
|
106
|
+
* dialect could differ from what the proxy later compares it against. Your own
|
|
107
|
+
* server, connected in process, is recorded in whichever era it offers.
|
|
95
108
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, inner]) => [key, canonical(inner)]));
|
|
99
|
-
return value;
|
|
100
|
-
}
|
|
109
|
+
const MODERN = { versionNegotiation: { mode: { pin: "2026-07-28" } } };
|
|
110
|
+
const EITHER = { versionNegotiation: { mode: "auto" } };
|
|
101
111
|
async function recordCapabilities(factory) {
|
|
102
112
|
const server = await factory();
|
|
103
113
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
@@ -105,10 +115,9 @@ async function recordCapabilities(factory) {
|
|
|
105
115
|
const client = new Client({
|
|
106
116
|
name: "mcp-authz-record-capabilities",
|
|
107
117
|
version: "1.0.0"
|
|
108
|
-
});
|
|
109
|
-
await client.connect(clientTransport);
|
|
118
|
+
}, EITHER);
|
|
110
119
|
try {
|
|
111
|
-
return await
|
|
120
|
+
return recordFrom(await listCatalogue(client, clientTransport));
|
|
112
121
|
} finally {
|
|
113
122
|
await client.close();
|
|
114
123
|
await server.close();
|
|
@@ -130,47 +139,30 @@ async function recordUpstream(url, options = {}) {
|
|
|
130
139
|
const client = new Client({
|
|
131
140
|
name: "mcp-authz-record-capabilities",
|
|
132
141
|
version: "1.0.0"
|
|
133
|
-
});
|
|
134
|
-
await client.connect(transport);
|
|
142
|
+
}, MODERN);
|
|
135
143
|
try {
|
|
136
|
-
return await
|
|
144
|
+
return recordFrom(await listCatalogue(client, transport));
|
|
137
145
|
} finally {
|
|
138
146
|
await client.close();
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
])
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
...prompts.prompts.map((prompt) => [`prompt:${prompt.name}`, prompt]),
|
|
159
|
-
...resources.resources.map((resource) => [`resource:${resource.name}`, resource]),
|
|
160
|
-
...templates.resourceTemplates.map((template) => [`resource:${template.name}`, template])
|
|
161
|
-
];
|
|
162
|
-
const resourceUris = Object.fromEntries([...resources.resources.map((resource) => [`resource:${resource.name}`, resource.uri]), ...templates.resourceTemplates.map((template) => [`resource:${template.name}`, template.uriTemplate])]);
|
|
163
|
-
const names = labelled.map(([label]) => label).sort();
|
|
164
|
-
const byLabel = new Map(labelled);
|
|
165
|
-
return {
|
|
166
|
-
names,
|
|
167
|
-
fingerprints: Object.fromEntries(names.map((label) => [label, digest({
|
|
168
|
-
label,
|
|
169
|
-
definition: byLabel.get(label)
|
|
170
|
-
})])),
|
|
171
|
-
resourceUris
|
|
172
|
-
};
|
|
173
|
-
}
|
|
149
|
+
/**
|
|
150
|
+
* The record, built from the listings as the server wrote them, which is what
|
|
151
|
+
* the proxy will compare against later.
|
|
152
|
+
*
|
|
153
|
+
* Built from entries rather than by assignment. A server may advertise a
|
|
154
|
+
* capability called `__proto__`, and `dict['__proto__'] = definition` runs the
|
|
155
|
+
* inherited setter instead of storing anything — losing exactly the record that
|
|
156
|
+
* would have caught that capability changing under you. `fromEntries` defines
|
|
157
|
+
* own properties, while the result stays an ordinary object.
|
|
158
|
+
*/
|
|
159
|
+
function recordFrom(listed) {
|
|
160
|
+
const names = [...listed.keys()].filter((label) => label !== INSTRUCTIONS).sort();
|
|
161
|
+
return {
|
|
162
|
+
names,
|
|
163
|
+
definitions: Object.fromEntries([...listed].map(([label, item]) => [label, definitionOf(item)])),
|
|
164
|
+
resourceUris: Object.fromEntries(names.filter((label) => label.startsWith("resource:")).map((label) => [label, String(listed.get(label).uri ?? listed.get(label).uriTemplate)]))
|
|
165
|
+
};
|
|
174
166
|
}
|
|
175
167
|
//#endregion
|
|
176
168
|
export { UNASSIGNED, recordCapabilities, recordUpstream, toPermissionsModule };
|
|
@@ -241,8 +241,15 @@ type ServerFactory<C> = ((context: C) => McpServer) & {
|
|
|
241
241
|
permissions: ReadonlyMap<string, string>;
|
|
242
242
|
routePermissions: ReadonlyMap<string, string>;
|
|
243
243
|
permissionForRoute: (kind: Capability, name: string) => string | undefined;
|
|
244
|
-
/**
|
|
245
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Every registration a concrete request name reaches, templates included, in
|
|
246
|
+
* registration order. More than one when an exact resource and a template
|
|
247
|
+
* cover the same URI.
|
|
248
|
+
*/
|
|
249
|
+
routesFor: (kind: Capability, name: string) => {
|
|
250
|
+
routeName: string;
|
|
251
|
+
permission: string;
|
|
252
|
+
}[];
|
|
246
253
|
};
|
|
247
254
|
/**
|
|
248
255
|
* Everything bound to one policy: `permission` accepts only what that policy can
|
package/package.json
CHANGED