mcp-authz 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.
- package/README.md +98 -16
- package/dist/cli.d.ts +2 -3
- package/dist/cli.js +1060 -16
- package/dist/definitions-CZVk-j1C.d.ts +5 -0
- package/dist/definitions-CyIy4YSZ.js +132 -0
- package/dist/index.d.ts +12 -8
- package/dist/index.js +16 -7
- package/dist/{ladder-CUzOKudC.js → ladder-6TnD3hTJ.js} +3 -1
- package/dist/node.d.ts +3 -4
- package/dist/openapi.d.ts +10 -11
- package/dist/{permissions-module-DxCHuE-N.d.ts → permissions-module-TOpt20D4.d.ts} +5 -5
- package/dist/proxy.d.ts +16 -4
- 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 +12 -7
- package/dist/testing.js +54 -62
- package/dist/{tools-BQE1O-7P.d.ts → tools-C1dZESYK.d.ts} +9 -2
- package/package.json +10 -12
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Plenty of good tools solve the neighbouring problems. Take one of them when its
|
|
|
22
22
|
| OAuth plumbing | official SDK, mcp-auth | Authentication and resource-server mechanics, with no permission model |
|
|
23
23
|
| Embedded dependency | **mcp-authz** | One package, and a policy engine that stays small by design |
|
|
24
24
|
|
|
25
|
-
Two cases need none of this. A stdio server on one laptop already has the OS account as its boundary. A server where every caller gets identical access wants one service credential and no policy.
|
|
25
|
+
Two cases need none of this. A stdio server on one laptop already has the OS account as its boundary. A server where every caller gets identical access wants one service credential and no policy. To have that stdio server show the model fewer tools, [`mcp-authz wrap`](#mcp-authz-wrap-fewer-tools-from-a-stdio-server) does that with no policy ([walkthrough](https://jagreehal.github.io/mcp-authz/wrap/)).
|
|
26
26
|
|
|
27
27
|
### You need one integration seam
|
|
28
28
|
|
|
@@ -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,14 +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
|
-
|
|
594
|
-
|
|
595
|
-
`@modelcontextprotocol/client` is an optional peer, needed only by this subpath.
|
|
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.
|
|
596
595
|
|
|
597
596
|
### `mcp-authz/openapi` — the same bet on an HTTP API
|
|
598
597
|
|
|
@@ -657,6 +656,7 @@ export default createMcpProxy({
|
|
|
657
656
|
verifier: { jwksUri: process.env.OAUTH_JWKS_URI! },
|
|
658
657
|
policy,
|
|
659
658
|
permissions: PERMISSIONS, // from recordUpstream → toPermissionsModule
|
|
659
|
+
definitions: DEFINITIONS, // same module; what each capability said when recorded
|
|
660
660
|
resourceUris: RESOURCE_URIS, // same module; a read names a URI, not a label
|
|
661
661
|
upstream: {
|
|
662
662
|
url: process.env.UPSTREAM_URL!,
|
|
@@ -665,15 +665,44 @@ export default createMcpProxy({
|
|
|
665
665
|
});
|
|
666
666
|
```
|
|
667
667
|
|
|
668
|
-
Record the upstream with `recordUpstream` or `mcp-authz record --upstream`,
|
|
669
|
-
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.
|
|
670
688
|
|
|
671
689
|
The proxy is stricter than embed mode, because nothing downstream of it re-checks
|
|
672
690
|
anything and it forwards on a service credential that outranks the caller.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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.
|
|
677
706
|
|
|
678
707
|
See [proxy mode](https://jagreehal.github.io/mcp-authz/typescript/proxy/) and the
|
|
679
708
|
[`proxy-example`](../../apps/proxy-example) app.
|
|
@@ -863,6 +892,59 @@ and on an invalid policy, and 0 on an unused permission, which stays a warning
|
|
|
863
892
|
because granting a role ahead of the tool that will use it is how a staged
|
|
864
893
|
rollout works.
|
|
865
894
|
|
|
895
|
+
### `mcp-authz wrap`: fewer tools from a stdio server
|
|
896
|
+
|
|
897
|
+
`wrap` sits in front of any stdio MCP server and hides the tools you leave out,
|
|
898
|
+
so a model reaches less than your API key allows.
|
|
899
|
+
|
|
900
|
+
```bash
|
|
901
|
+
npx -y mcp-authz tools --out cases.jsonc -- npx -y @acme/cases-mcp
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
`tools` saves the server's tools to `cases.jsonc`, one line each with what the
|
|
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
|
|
913
|
+
prints the `mcpServers` entry to paste, or writes it into a client config file
|
|
914
|
+
with `--client-out .mcp.json`:
|
|
915
|
+
|
|
916
|
+
```json
|
|
917
|
+
"cases": {
|
|
918
|
+
"command": "npx",
|
|
919
|
+
"args": ["-y", "mcp-authz", "wrap", "/Users/you/mcp/cases.jsonc"],
|
|
920
|
+
"env": { "CASES_API_KEY": "..." }
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
`wrap` drops unlisted tools from `tools/list` and answers a call to one with an
|
|
925
|
+
error that names it, so the server never receives it. Tools the server adds
|
|
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://…`.
|
|
942
|
+
|
|
943
|
+
`wrap` limits one session; scope the key itself where the service supports it.
|
|
944
|
+
When the client disconnects, `wrap` stops the whole process tree, `npx` and the
|
|
945
|
+
server it started. The [walkthrough](https://jagreehal.github.io/mcp-authz/wrap/)
|
|
946
|
+
covers the rest.
|
|
947
|
+
|
|
866
948
|
### `mcp-authz/policy`
|
|
867
949
|
|
|
868
950
|
The policy half on its own (`definePolicy`, `definePermissions`,
|
package/dist/cli.d.ts
CHANGED