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 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, 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,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. `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.
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`, price
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
- Routing headers that disagree with the body are refused rather than forwarded; a
672
- request with no routing headers is authorized from the body, which is what the
673
- upstream will act on; and a capability the map does not price is refused outright.
674
- 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.
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 and what the server says about it, destructive ones commented out.
875
- A schema beside it gives your editor completion and typo checks. `tools` also
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", "--config", "/Users/you/mcp/cases.jsonc"],
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. `mcp-authz tools --check cases.jsonc` reports
890
- what changed on the server since you saved, and `tools --config cases.jsonc
891
- --refresh` records it, keeping your choices. For a quick trial, `wrap --deny
892
- a,b -- <command>` takes the list as arguments.
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