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 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, fingerprints } = await recordCapabilities(() => buildServer(TEST_CONFIG));
569
+ const { names, definitions } = await recordCapabilities(() => buildServer(TEST_CONFIG));
570
570
 
571
571
  expect(names).toEqual(Object.keys(PERMISSIONS).sort());
572
- expect(fingerprints).toMatchSnapshot();
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. `fingerprints` covers the one it cannot see — a
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 digests the whole definition
591
- as served, so a snapshot turns that into a diff on the pull request. Nothing is
592
- enforced at boot: a digest in production is a second source of truth, and would
593
- make a description edit an outage.
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`, price
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
- Routing headers that disagree with the body are refused rather than forwarded; a
674
- request with no routing headers is authorized from the body, which is what the
675
- upstream will act on; and a capability the map does not price is refused outright.
676
- The caller's `Authorization` and `Cookie` stay at the edge.
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
@@ -1,4 +1,3 @@
1
1
  //#region src/cli.d.ts
2
- declare function main(argv: readonly string[]): number | Promise<number>;
3
- //#endregion
4
- export { main };
2
+ export declare function main(argv: readonly string[]): number | Promise<number>;
3
+ //#endregion