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/README.md
CHANGED
|
@@ -566,10 +566,10 @@ reads it off the server instead.
|
|
|
566
566
|
```ts
|
|
567
567
|
import { recordCapabilities, toPermissionsModule } from 'mcp-authz/testing';
|
|
568
568
|
|
|
569
|
-
const { names,
|
|
569
|
+
const { names, definitions } = await recordCapabilities(() => buildServer(TEST_CONFIG));
|
|
570
570
|
|
|
571
571
|
expect(names).toEqual(Object.keys(PERMISSIONS).sort());
|
|
572
|
-
expect(
|
|
572
|
+
expect(definitions).toMatchSnapshot();
|
|
573
573
|
```
|
|
574
574
|
|
|
575
575
|
It builds your server, connects a client over an in-memory transport, and lists
|
|
@@ -585,12 +585,13 @@ a person has decided what each one costs. Permissions are never guessed from
|
|
|
585
585
|
decisions must not be made from them.
|
|
586
586
|
|
|
587
587
|
`gate()` already refuses to start on a capability with no price, which covers a
|
|
588
|
-
dependency that adds a tool. `
|
|
588
|
+
dependency that adds a tool. `definitions` covers the one it cannot see — a
|
|
589
589
|
capability that keeps its name while its description, input schema, prompt
|
|
590
|
-
arguments or URI template change underneath. Each
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
590
|
+
arguments or URI template change underneath. Each holds the whole definition as
|
|
591
|
+
served (key order sorted, `_meta` and `icons` left out), so a snapshot turns that
|
|
592
|
+
into a diff of the exact words on the pull request. With `gate()` in your own
|
|
593
|
+
server, that snapshot is the check; `createMcpProxy` enforces the same record at
|
|
594
|
+
runtime.
|
|
594
595
|
|
|
595
596
|
### `mcp-authz/openapi` — the same bet on an HTTP API
|
|
596
597
|
|
|
@@ -655,6 +656,7 @@ export default createMcpProxy({
|
|
|
655
656
|
verifier: { jwksUri: process.env.OAUTH_JWKS_URI! },
|
|
656
657
|
policy,
|
|
657
658
|
permissions: PERMISSIONS, // from recordUpstream → toPermissionsModule
|
|
659
|
+
definitions: DEFINITIONS, // same module; what each capability said when recorded
|
|
658
660
|
resourceUris: RESOURCE_URIS, // same module; a read names a URI, not a label
|
|
659
661
|
upstream: {
|
|
660
662
|
url: process.env.UPSTREAM_URL!,
|
|
@@ -663,15 +665,44 @@ export default createMcpProxy({
|
|
|
663
665
|
});
|
|
664
666
|
```
|
|
665
667
|
|
|
666
|
-
Record the upstream with `recordUpstream` or `mcp-authz record --upstream`,
|
|
667
|
-
the map, deploy the proxy.
|
|
668
|
+
Record the upstream with `recordUpstream` or `mcp-authz record --upstream`, which
|
|
669
|
+
connect as a 2026-07-28 client, price the map, deploy the proxy. `record --check`
|
|
670
|
+
exits 1 when the upstream drifts, when its instructions change, and when a priced
|
|
671
|
+
capability has no entry in `DEFINITIONS`.
|
|
672
|
+
|
|
673
|
+
The proxy speaks MCP 2026-07-28 only: anything but a POST is a 405, and a
|
|
674
|
+
request without validated `Mcp-Method` and `Mcp-Name` routing headers is a 400.
|
|
675
|
+
Clients that still open with the 2025 `initialize` handshake cannot use it.
|
|
676
|
+
|
|
677
|
+
`definitions` is required, and the proxy refuses to boot if a priced label has
|
|
678
|
+
none. An upstream can keep a name you approved and rewrite the description to
|
|
679
|
+
steer the model, or add an argument to carry data out. So a capability whose
|
|
680
|
+
definition differs from the record is left out of listings, with a
|
|
681
|
+
`console.warn` naming it and the changed fields. A call to a capability not
|
|
682
|
+
checked in the last 60 seconds makes the proxy list the upstream itself first;
|
|
683
|
+
one that changed or is no longer listed is refused with a 403. The upstream's
|
|
684
|
+
instructions are held to `DEFINITIONS['server:instructions']` and removed when
|
|
685
|
+
they differ. Re-recording is how a change is approved. This catches a change in
|
|
686
|
+
what the model is told; it does not prove a remote tool behaves as it did, or
|
|
687
|
+
that its output is free of prompt injection.
|
|
668
688
|
|
|
669
689
|
The proxy is stricter than embed mode, because nothing downstream of it re-checks
|
|
670
690
|
anything and it forwards on a service credential that outranks the caller.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
691
|
+
Methods are on an explicit list: listings, `server/discover`, `ping` and two
|
|
692
|
+
notifications pass; calls, prompt gets and reads are priced; a
|
|
693
|
+
`completion/complete` is priced as the prompt or resource it completes, and a
|
|
694
|
+
`subscriptions/listen` as a read of each URI it names, scopes included; anything else is a 400
|
|
695
|
+
with `-32601`. Routing headers that disagree with the body, and a body that
|
|
696
|
+
repeats a JSON key, are refused rather than forwarded. A read must satisfy every
|
|
697
|
+
priced resource covering its URI. A capability the map does not price is refused
|
|
698
|
+
outright. Filtered listings are marked private and `no-store`.
|
|
699
|
+
A `tools/call` whose arguments break the recorded `inputSchema` is a 400.
|
|
700
|
+
Answers are screened: `structuredContent` that breaks the recorded
|
|
701
|
+
`outputSchema` is withheld, and output carrying invisible characters or text
|
|
702
|
+
addressed to the model arrives after a notice to treat it as data. That notice
|
|
703
|
+
is not a guarantee, and nothing verifies what a remote tool actually does; keep
|
|
704
|
+
the service credential to least privilege. The caller's
|
|
705
|
+
`Authorization` and `Cookie` stay at the edge.
|
|
675
706
|
|
|
676
707
|
See [proxy mode](https://jagreehal.github.io/mcp-authz/typescript/proxy/) and the
|
|
677
708
|
[`proxy-example`](../../apps/proxy-example) app.
|
|
@@ -871,25 +902,43 @@ npx -y mcp-authz tools --out cases.jsonc -- npx -y @acme/cases-mcp
|
|
|
871
902
|
```
|
|
872
903
|
|
|
873
904
|
`tools` saves the server's tools to `cases.jsonc`, one line each with what the
|
|
874
|
-
tool does
|
|
875
|
-
|
|
905
|
+
tool does, what the server says about it and roughly how many tokens its
|
|
906
|
+
definition costs the model. Only tools the server marks read-only start switched
|
|
907
|
+
on; destructive, unknown and flagged ones start commented out, and a tool that
|
|
908
|
+
claims both read-only and destructive counts as destructive. The marks are the
|
|
909
|
+
server's claims, shown to help you choose, not an approval.
|
|
910
|
+
A schema beside it gives your editor completion and typo checks, and records
|
|
911
|
+
each tool's definition and the server's instructions; `wrap` will not start
|
|
912
|
+
without it. `tools` also
|
|
876
913
|
prints the `mcpServers` entry to paste, or writes it into a client config file
|
|
877
914
|
with `--client-out .mcp.json`:
|
|
878
915
|
|
|
879
916
|
```json
|
|
880
917
|
"cases": {
|
|
881
918
|
"command": "npx",
|
|
882
|
-
"args": ["-y", "mcp-authz", "wrap", "
|
|
919
|
+
"args": ["-y", "mcp-authz", "wrap", "/Users/you/mcp/cases.jsonc"],
|
|
883
920
|
"env": { "CASES_API_KEY": "..." }
|
|
884
921
|
}
|
|
885
922
|
```
|
|
886
923
|
|
|
887
924
|
`wrap` drops unlisted tools from `tools/list` and answers a call to one with an
|
|
888
925
|
error that names it, so the server never receives it. Tools the server adds
|
|
889
|
-
later stay hidden until you list them.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
926
|
+
later stay hidden until you list them. So does a tool whose description or
|
|
927
|
+
schema has changed since you saved it, which stops a rug pull: a server can't
|
|
928
|
+
keep an approved name and rewrite what it tells the model. A call to a tool
|
|
929
|
+
`wrap` has not yet checked waits while `wrap` lists the server itself, so a
|
|
930
|
+
client that calls without listing cannot skip the check, and a
|
|
931
|
+
`list_changed` from the server means every tool is checked again. Instructions
|
|
932
|
+
that differ from the record are removed from the server's reply. Arguments that
|
|
933
|
+
break the recorded `inputSchema` are refused, `structuredContent` that breaks
|
|
934
|
+
the `outputSchema` is withheld, and output carrying text addressed to the model
|
|
935
|
+
arrives after a notice to treat it as data. The notice catches only obvious
|
|
936
|
+
injections, and nothing checks what a tool actually does.
|
|
937
|
+
`mcp-authz tools --check cases.jsonc` reports
|
|
938
|
+
what changed on the server since you saved, and `tools --refresh cases.jsonc`
|
|
939
|
+
records it, keeping your choices. For a quick trial, `wrap --deny
|
|
940
|
+
a,b -- <command>` takes the list as arguments. For a remote server, wrap the
|
|
941
|
+
bridge: `-- npx -y mcp-remote https://…`.
|
|
893
942
|
|
|
894
943
|
`wrap` limits one session; scope the key itself where the service supports it.
|
|
895
944
|
When the client disconnects, `wrap` stops the whole process tree, `npx` and the
|