You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add the Admission seam (bounded rewrite R1, the highest-regression-risk
change in this refactor): an Admission interface plus a Cedar-backed
implementation that wraps the existingauthorizers.Authorizer (no new
policy model). Wire it into the VMCP core so List* (filter) and Call/Read/Get (deny) enforce the same authorization decision — closing the
"list says yes / call says no" gap that today is only achievable because authz
runs as HTTP middleware on a single path. This re-platforms enforcement from two
middleware (AuthzMiddleware + AnnotationEnrichmentMiddleware) into the core
while preserving byte-for-byte authorization behavior.
Context
Today authorization runs only as HTTP middleware (Config.AuthzMiddleware,
built by factory.NewIncomingAuthMiddleware, Cedar-based) and is applied
conditionally in Handler (server.go:606), and it depends on the
annotation-enrichment middleware running first to inject Tool.Annotations
into context so Cedar when-clauses (e.g. resource.readOnlyHint) evaluate.
The RFC requires that ListTools and CallTool enforce the same decision from
one source. Per architecture.md "Admission seam (R1)" (lines 140-198) and the
resolved decision in research.md (R1 → core admission seam), the chosen approach
is a core-level admission seam fed by the existing Cedar authorizer — reusing
the policy model and factory, not introducing a new one. The core already holds
the aggregated Tool.Annotations, so it sources them directly and the
annotation-enrichment middleware is retired on the domain path.
This task adds the seam and wires it into the core methods implemented in #5437. The authz/annotation HTTP middleware stays in the live server.New
path until #5441 removes it (Phase 2) — so there is temporary
double-enforcement only inside tests that exercise the core directly, never
in the shipped server.New path.
Parent Story: ##5430 Dependencies: ##5437 (the New(cfg) -> VMCP core + method bodies the seam wires into) Blocks: #5441 (removes the authz + annotation-enrichment HTTP middleware once this core seam exists)
Acceptance Criteria
Admission interface defined in pkg/vmcp/admission.go with the six methods (FilterTools/AllowToolCall/FilterResources/AllowResourceRead/FilterPrompts/AllowPromptGet), domain-typed (no mcp-go types).
A Cedar-backed implementation wraps the existing authorizers.Authorizer built from cfg.Authz via the existing factory (newCedarAuthzMiddleware's authorizer construction); no new policy language.
Admission wired into the core's List* (filter) and Call/Read/Get (deny) so list and call enforce the same decision from one source; Lookup* apply the same filter (never resolve a denied capability).
The adapter re-injects the explicit identity into the ctx passed to the authorizer (auth.WithIdentity(ctx, identity)) before each AuthorizeWithJWTClaims call — an internal adapter detail, not a public-path context read.
Tool annotations are sourced directly from the core's Tool.Annotations and injected via authorizers.WithToolAnnotations(ctx, convertAnnotations(...)), reusing the existing converter — replacing AnnotationEnrichmentMiddleware.
No-op / allow-all when cfg.Authz is absent (parity with today's cfg.AuthzMiddleware != nil guard): FilterX returns the input unchanged, AllowX returns true.
passThroughTools (optimizer meta-tools find_tool/call_tool) remain exempt, flowing into the seam the same way they flow into the authz middleware today.
PR is ≤ 400 LOC and ≤ 10 files changed (excluding tests/docs/generated). Sizing is borderline (the Admission interface + the Cedar adapter + six wire-in points across List*/Call/Read/Get/Lookup*); tests are excluded from the cap, which keeps it
plausible as one PR — if production LOC exceeds the limit, split via /split-pr (e.g.
interface + Cedar adapter first, then the core wire-in) (C1).
server.New signature and observable behavior unchanged.
All tests pass (task test); lint clean (task lint-fix).
Code reviewed and approved.
Technical Approach
Recommended Implementation
Add pkg/vmcp/admission.go with the Admission interface and a Cedar-backed cedarAdmission implementation. The implementation holds an authorizers.Authorizer constructed from the samecfg.Authz using the
existing factory path (newCedarAuthzMiddleware, pkg/vmcp/auth/factory/incoming.go:107).
Mirror the exact list-filter and call-deny logic from pkg/authz:
FilterTools mirrors filterToolsByPolicy (tool_filter.go:19): for each
tool, inject annotations then call AuthorizeWithJWTClaims(MCPFeatureTool, MCPOperationCall, name, nil); keep only authorized tools. On a per-tool
authorizer error, log-and-skip exactly as today (the tool is omitted, not a
hard failure).
AllowResourceRead / FilterResources use MCPOperationRead; AllowPromptGet / FilterPrompts use the prompt/get feature+operation pair —
matching the existing authorizers feature/operation constants.
Two bridges from the explicit-param domain style to the authorizer's context
contract, both internal adapter details (the public Admission methods take
identity explicitly):
Identity → context re-injection. Cedar's AuthorizeWithJWTClaims reads auth.IdentityFromContext(ctx) (cedar/core.go:842). The adapter writes the
explicit identity into ctx — ctx = auth.WithIdentity(ctx, identity) — before
each call. This does NOT reintroduce anti-pattern fix(typo): corrects readme #1 on the public path.
Annotation source (replaces AnnotationEnrichmentMiddleware). The core
already holds aggregated Tool.Annotations (types.go:383). The adapter
injects them via authorizers.WithToolAnnotations(ctx, convertAnnotations(tool.Annotations)), reusing the existing converter
(annotation_enrichment.go:92) — same data, same shape, no middleware. Only
inject when the tool has hint fields (matching the existing hasAnyHint/nil-returning converter behavior).
New builds a nil/allow-all Admission when cfg.Authz is absent (or has no
Cedar policies), matching today's conditional guard; the no-op FilterX returns
input unchanged and AllowX returns true, exactly like the nil-authorizer
no-ops at tool_filter.go:20,67. passThroughTools are threaded into the seam
the same way they pass into newCedarAuthzMiddleware(cfg.Authz, passThroughTools)
today (incoming.go:89), so they stay exempt.
Patterns & Frameworks
Reuse the existing Cedar authorizer and pkg/authz decision logic — wrap, do
not reimplement. No new policy language (RFC R1; architecture.md "How the
decision is computed").
stdlib testing.T + testify, not Ginkgo, in pkg/vmcp (R8). The authorizers.Authorizer single-method interface is best stubbed with a
hand-rolled mock (as tool_filter_test.go does) rather than gomock.
Conventions: .claude/rules/go-style.md (SPDX header on the new .go file;
copy-before-mutating args/meta maps), .claude/rules/vmcp-anti-patterns.md, .claude/rules/security.md (never log identity/tokens).
pkg/authz/tool_filter.go:19 — filterToolsByPolicy — the exact list-filter logic to mirror (per-tool annotation injection + AuthorizeWithJWTClaims; nil-authorizer no-op at line 20).
pkg/authz/tool_filter.go:64 — authorizeToolCall — the call-deny logic to mirror (MCPFeatureTool/MCPOperationCall; nil-authorizer no-op returning true at line 67).
pkg/authz/authorizers/core.go:48 — authorizers.Authorizer interface (the single AuthorizeWithJWTClaims method the seam wraps); MCPFeature/MCPOperation constants nearby.
pkg/authz/authorizers/cedar/core.go:842 — AuthorizeWithJWTClaims reads auth.IdentityFromContext(ctx) (returns ErrMissingPrincipal if absent) — the reason the adapter must re-inject identity into ctx.
pkg/vmcp/auth/factory/incoming.go:89 — newCedarAuthzMiddleware(cfg.Authz, passThroughTools) call (factory NewIncomingAuthMiddleware at line 50; newCedarAuthzMiddleware decl at line 107) — the existing factory the seam reuses to build the authorizer from cfg.Authz, and where passThroughTools enters today.
pkg/vmcp/server/annotation_enrichment.go:92 — convertAnnotations (vmcp ToolAnnotations → authorizers.ToolAnnotations); reuse it. The middleware it lives in is retired on the domain path.
pkg/authz/authorizers/annotations.go:40 — authorizers.WithToolAnnotations(ctx, ...) — how annotations are placed on ctx for Cedar when-clauses.
pkg/vmcp/types.go:383 — Tool.Annotations *ToolAnnotations — the core-held annotation source replacing the middleware-injected one.
pkg/vmcp/server/server.go:606 — today's if s.config.AuthzMiddleware != nil conditional (the no-op-when-unconfigured parity to preserve; second use at 614 for annotation-enrichment).
pkg/vmcp/cli/serve.go:356-362 — passThroughTools: line 356 is the var decl; the literal entries (optimizerdec.FindToolName/optimizerdec.CallToolName, i.e. find_tool/call_tool) are at 357-362; passed to NewIncomingAuthMiddleware at line 375.
pkg/authz/tool_filter_test.go — the test file to mirror (hand-rolled mockAuthorizer stub at line ~20; TestFilterToolsByPolicy, TestFilterToolsByPolicy_WithCedarAuthorizer, TestAuthorizeToolCall*) — reproduce its cases against the core/admission seam.
The Admission seam contract (from architecture.md, lines 146-161). Domain
types only — no mcp-go, no new policy model.
// Admission decides whether an identity may see/use a capability. It wraps the// existing authorizers.Authorizer (Cedar); it does NOT define a new policy model.typeAdmissioninterface {
// FilterTools returns the subset of tools the identity may call. Mirrors// pkg/authz filterToolsByPolicy: per-tool AuthorizeWithJWTClaims(call) using// the tool's annotations.FilterTools(ctx context.Context, identity*auth.Identity, tools []Tool) ([]Tool, error)
// AllowToolCall mirrors pkg/authz authorizeToolCall (MCPFeatureTool/Call).AllowToolCall(ctx context.Context, identity*auth.Identity, tool*Tool, argsmap[string]any) (bool, error)
// Resource/Prompt equivalents (MCPOperationRead / MCPOperationGet).FilterResources(ctx context.Context, identity*auth.Identity, rs []Resource) ([]Resource, error)
AllowResourceRead(ctx context.Context, identity*auth.Identity, r*Resource) (bool, error)
FilterPrompts(ctx context.Context, identity*auth.Identity, ps []Prompt) ([]Prompt, error)
AllowPromptGet(ctx context.Context, identity*auth.Identity, p*Prompt) (bool, error)
}
// Internal adapter detail (illustrative — not part of the public contract):// before delegating to the wrapped authorizer, re-inject identity + annotations// into the ctx the authorizer reads.// ctx = auth.WithIdentity(ctx, identity)// if ann := convertAnnotations(tool.Annotations); ann != nil {// ctx = authorizers.WithToolAnnotations(ctx, ann)// }// ok, err := a.AuthorizeWithJWTClaims(ctx, authorizers.MCPFeatureTool,// authorizers.MCPOperationCall, tool.Name, args)//// no-op impl: FilterX returns input unchanged; AllowX returns (true, nil).
Testing Strategy
Mirror pkg/authz/tool_filter_test.go (hand-rolled mockAuthorizer stub +
testify) against the core/admission seam. The dedicated R1 security-parity tests
are required.
Unit Tests (admission seam + core wiring)
R1 (1) — list/call same decision: a tool denied by Cedar is omitted from ListTools AND denied by CallTool (the "list says yes / call says no" elimination is closed; both consult the same Admission). Lookup* for a denied tool returns the unknown/unadvertised error.
R1 (2) — annotation-aware when-clauses: policies keyed on annotations (e.g. resource.readOnlyHint) evaluate identically using core-sourced Tool.Annotations (via the reused convertAnnotations + WithToolAnnotations) as they did with middleware-injected annotations. Cover a readOnlyHint-gated allow and deny.
R1 (3) — no-op/allow-all when no authz configured: with cfg.Authz absent (or no Cedar policies), FilterTools/FilterResources/FilterPrompts return the input unchanged and AllowToolCall/AllowResourceRead/AllowPromptGet return true (parity with the AuthzMiddleware != nil guard and the nil-authorizer no-ops at tool_filter.go:20,67).
R1 (4) — passThroughTools exempt: optimizer meta-tools (find_tool/call_tool) are not denied by the seam, matching their exemption through newCedarAuthzMiddleware today.
R1 (5) — identity never logged: assert no identity/token material appears in emitted logs (capture slog output); the seam passes *auth.Identity through unchanged (it redacts Token/UpstreamTokens).
Authorizer invoked with the correct feature/operation per method (MCPFeatureTool/MCPOperationCall, MCPOperationRead, prompt/get) and the re-injected identity present in the ctx it receives (mirrors TestFilterToolsByPolicy_CallsAuthorizerCorrectly).
Per-tool authorizer error during FilterTools causes that tool to be skipped (log-and-continue), not a hard failure — matching filterToolsByPolicy.
Resource-read / prompt-get parity cases mirroring the tool cases against the Cedar authorizer (TestAuthorizeToolCall_WithCedarAuthorizer).
Integration / Behavioral Parity Tests
Driving the stable server.New wrapper, MCP responses (tools/list, tools/call, resources, prompts) under a Cedar policy remain equivalent before/after — the live path still uses the HTTP authz middleware in Phase 1 (removed in P2.3 Move middleware chain under Serve; remove authz + annotation mw #5441), so this asserts the wrapper is unchanged while the new core seam is exercised directly in unit tests.
Edge Cases
Cedar AuthorizeWithJWTClaims returns ErrMissingPrincipal when identity is absent from ctx — confirm the adapter always re-injects so this only surfaces for a genuinely nil identity, and that nil/anonymous identity behaves consistently between filter and call.
Empty tool/resource/prompt list in/out; nil Tool.Annotations (converter returns nil, no annotation ctx write).
Tool with hint fields vs without — annotations injected only when present (matching the existing hasAnyHint gate).
Description
Add the
Admissionseam (bounded rewrite R1, the highest-regression-riskchange in this refactor): an
Admissioninterface plus a Cedar-backedimplementation that wraps the existing
authorizers.Authorizer(no newpolicy model). Wire it into the
VMCPcore soList*(filter) andCall/Read/Get(deny) enforce the same authorization decision — closing the"list says yes / call says no" gap that today is only achievable because authz
runs as HTTP middleware on a single path. This re-platforms enforcement from two
middleware (
AuthzMiddleware+AnnotationEnrichmentMiddleware) into the corewhile preserving byte-for-byte authorization behavior.
Context
Today authorization runs only as HTTP middleware (
Config.AuthzMiddleware,built by
factory.NewIncomingAuthMiddleware, Cedar-based) and is appliedconditionally in
Handler(server.go:606), and it depends on theannotation-enrichment middleware running first to inject
Tool.Annotationsinto context so Cedar
when-clauses (e.g.resource.readOnlyHint) evaluate.The RFC requires that
ListToolsandCallToolenforce the same decision fromone source. Per architecture.md "Admission seam (R1)" (lines 140-198) and the
resolved decision in research.md (R1 → core admission seam), the chosen approach
is a core-level admission seam fed by the existing Cedar authorizer — reusing
the policy model and factory, not introducing a new one. The core already holds
the aggregated
Tool.Annotations, so it sources them directly and theannotation-enrichment middleware is retired on the domain path.
This task adds the seam and wires it into the core methods implemented in
#5437. The authz/annotation HTTP middleware stays in the live
server.Newpath until #5441 removes it (Phase 2) — so there is temporary
double-enforcement only inside tests that exercise the core directly, never
in the shipped
server.Newpath.Parent Story: ##5430
Dependencies: ##5437 (the
New(cfg) -> VMCPcore + method bodies the seam wires into)Blocks: #5441 (removes the authz + annotation-enrichment HTTP middleware once this core seam exists)
Acceptance Criteria
Admissioninterface defined inpkg/vmcp/admission.gowith the six methods (FilterTools/AllowToolCall/FilterResources/AllowResourceRead/FilterPrompts/AllowPromptGet), domain-typed (no mcp-go types).authorizers.Authorizerbuilt fromcfg.Authzvia the existing factory (newCedarAuthzMiddleware's authorizer construction); no new policy language.Admissionwired into the core'sList*(filter) andCall/Read/Get(deny) so list and call enforce the same decision from one source;Lookup*apply the same filter (never resolve a denied capability).auth.WithIdentity(ctx, identity)) before eachAuthorizeWithJWTClaimscall — an internal adapter detail, not a public-path context read.Tool.Annotationsand injected viaauthorizers.WithToolAnnotations(ctx, convertAnnotations(...)), reusing the existing converter — replacingAnnotationEnrichmentMiddleware.cfg.Authzis absent (parity with today'scfg.AuthzMiddleware != nilguard):FilterXreturns the input unchanged,AllowXreturnstrue.passThroughTools(optimizer meta-toolsfind_tool/call_tool) remain exempt, flowing into the seam the same way they flow into the authz middleware today.borderline (the
Admissioninterface + the Cedar adapter + six wire-in points acrossList*/Call/Read/Get/Lookup*); tests are excluded from the cap, which keeps itplausible as one PR — if production LOC exceeds the limit, split via
/split-pr(e.g.interface + Cedar adapter first, then the core wire-in) (C1).
server.Newsignature and observable behavior unchanged.task test); lint clean (task lint-fix).Technical Approach
Recommended Implementation
Add
pkg/vmcp/admission.gowith theAdmissioninterface and a Cedar-backedcedarAdmissionimplementation. The implementation holds anauthorizers.Authorizerconstructed from the samecfg.Authzusing theexisting factory path (
newCedarAuthzMiddleware,pkg/vmcp/auth/factory/incoming.go:107).Mirror the exact list-filter and call-deny logic from
pkg/authz:FilterToolsmirrorsfilterToolsByPolicy(tool_filter.go:19): for eachtool, inject annotations then call
AuthorizeWithJWTClaims(MCPFeatureTool, MCPOperationCall, name, nil); keep only authorized tools. On a per-toolauthorizer error, log-and-skip exactly as today (the tool is omitted, not a
hard failure).
AllowToolCallmirrorsauthorizeToolCall(tool_filter.go:64):AuthorizeWithJWTClaims(MCPFeatureTool, MCPOperationCall, name, args).AllowResourceRead/FilterResourcesuseMCPOperationRead;AllowPromptGet/FilterPromptsuse the prompt/get feature+operation pair —matching the existing
authorizersfeature/operation constants.Two bridges from the explicit-param domain style to the authorizer's context
contract, both internal adapter details (the public
Admissionmethods takeidentity explicitly):
AuthorizeWithJWTClaimsreadsauth.IdentityFromContext(ctx)(cedar/core.go:842). The adapter writes theexplicit identity into ctx —
ctx = auth.WithIdentity(ctx, identity)— beforeeach call. This does NOT reintroduce anti-pattern fix(typo): corrects readme #1 on the public path.
AnnotationEnrichmentMiddleware). The corealready holds aggregated
Tool.Annotations(types.go:383). The adapterinjects them via
authorizers.WithToolAnnotations(ctx, convertAnnotations(tool.Annotations)), reusing the existing converter(
annotation_enrichment.go:92) — same data, same shape, no middleware. Onlyinject when the tool has hint fields (matching the existing
hasAnyHint/nil-returning converter behavior).Newbuilds a nil/allow-allAdmissionwhencfg.Authzis absent (or has noCedar policies), matching today's conditional guard; the no-op
FilterXreturnsinput unchanged and
AllowXreturnstrue, exactly like the nil-authorizerno-ops at
tool_filter.go:20,67.passThroughToolsare threaded into the seamthe same way they pass into
newCedarAuthzMiddleware(cfg.Authz, passThroughTools)today (
incoming.go:89), so they stay exempt.Patterns & Frameworks
pkg/authzdecision logic — wrap, donot reimplement. No new policy language (RFC R1; architecture.md "How the
decision is computed").
VMCPboundary(anti-pattern Implement secret injection #5).
Admissionis a decorator-style seam consulted by the core;it can only subtract reachability (filter list output / refuse a call),
never widen access (Core Principle Bump golangci/golangci-lint-action from 2f856675483cb8b9378ee77ee0beb67955aca9d7 to 4696ba8babb6127d732c3c6dde519db15edab9ea #3).
testing.T+ testify, not Ginkgo, inpkg/vmcp(R8). Theauthorizers.Authorizersingle-method interface is best stubbed with ahand-rolled mock (as
tool_filter_test.godoes) rather than gomock..claude/rules/go-style.md(SPDX header on the new.gofile;copy-before-mutating args/meta maps),
.claude/rules/vmcp-anti-patterns.md,.claude/rules/security.md(never log identity/tokens).Code Pointers
pkg/vmcp/admission.go(new) —Admissioninterface + Cedar-backed impl. See architecture.md "Admission seam (R1)", lines 140-198.pkg/authz/tool_filter.go:19—filterToolsByPolicy— the exact list-filter logic to mirror (per-tool annotation injection +AuthorizeWithJWTClaims; nil-authorizer no-op at line 20).pkg/authz/tool_filter.go:64—authorizeToolCall— the call-deny logic to mirror (MCPFeatureTool/MCPOperationCall; nil-authorizer no-op returningtrueat line 67).pkg/authz/authorizers/core.go:48—authorizers.Authorizerinterface (the singleAuthorizeWithJWTClaimsmethod the seam wraps);MCPFeature/MCPOperationconstants nearby.pkg/authz/authorizers/cedar/core.go:842—AuthorizeWithJWTClaimsreadsauth.IdentityFromContext(ctx)(returnsErrMissingPrincipalif absent) — the reason the adapter must re-inject identity into ctx.pkg/vmcp/auth/factory/incoming.go:89—newCedarAuthzMiddleware(cfg.Authz, passThroughTools)call (factoryNewIncomingAuthMiddlewareat line 50;newCedarAuthzMiddlewaredecl at line 107) — the existing factory the seam reuses to build the authorizer fromcfg.Authz, and wherepassThroughToolsenters today.pkg/vmcp/server/annotation_enrichment.go:92—convertAnnotations(vmcpToolAnnotations→authorizers.ToolAnnotations); reuse it. The middleware it lives in is retired on the domain path.pkg/authz/authorizers/annotations.go:40—authorizers.WithToolAnnotations(ctx, ...)— how annotations are placed on ctx for Cedarwhen-clauses.pkg/vmcp/types.go:383—Tool.Annotations *ToolAnnotations— the core-held annotation source replacing the middleware-injected one.pkg/vmcp/server/server.go:606— today'sif s.config.AuthzMiddleware != nilconditional (the no-op-when-unconfigured parity to preserve; second use at 614 for annotation-enrichment).pkg/vmcp/cli/serve.go:356-362—passThroughTools: line 356 is thevardecl; the literal entries (optimizerdec.FindToolName/optimizerdec.CallToolName, i.e.find_tool/call_tool) are at 357-362; passed toNewIncomingAuthMiddlewareat line 375.pkg/authz/tool_filter_test.go— the test file to mirror (hand-rolledmockAuthorizerstub at line ~20;TestFilterToolsByPolicy,TestFilterToolsByPolicy_WithCedarAuthorizer,TestAuthorizeToolCall*) — reproduce its cases against the core/admission seam.pkg/vmcp/core.go(from P1.4 New(cfg) -> VMCP core constructor #5437) — the*coreVMCPmethods (List*/Call/Read/Get/Lookup*) the seam is wired into.Component Interfaces
The
Admissionseam contract (from architecture.md, lines 146-161). Domaintypes only — no mcp-go, no new policy model.
Testing Strategy
Mirror
pkg/authz/tool_filter_test.go(hand-rolledmockAuthorizerstub +testify) against the core/admission seam. The dedicated R1 security-parity tests
are required.
Unit Tests (admission seam + core wiring)
ListToolsAND denied byCallTool(the "list says yes / call says no" elimination is closed; both consult the sameAdmission).Lookup*for a denied tool returns the unknown/unadvertised error.when-clauses: policies keyed on annotations (e.g.resource.readOnlyHint) evaluate identically using core-sourcedTool.Annotations(via the reusedconvertAnnotations+WithToolAnnotations) as they did with middleware-injected annotations. Cover areadOnlyHint-gated allow and deny.cfg.Authzabsent (or no Cedar policies),FilterTools/FilterResources/FilterPromptsreturn the input unchanged andAllowToolCall/AllowResourceRead/AllowPromptGetreturntrue(parity with theAuthzMiddleware != nilguard and the nil-authorizer no-ops attool_filter.go:20,67).passThroughToolsexempt: optimizer meta-tools (find_tool/call_tool) are not denied by the seam, matching their exemption throughnewCedarAuthzMiddlewaretoday.slogoutput); the seam passes*auth.Identitythrough unchanged (it redactsToken/UpstreamTokens).MCPFeatureTool/MCPOperationCall,MCPOperationRead, prompt/get) and the re-injected identity present in the ctx it receives (mirrorsTestFilterToolsByPolicy_CallsAuthorizerCorrectly).FilterToolscauses that tool to be skipped (log-and-continue), not a hard failure — matchingfilterToolsByPolicy.TestAuthorizeToolCall_WithCedarAuthorizer).Integration / Behavioral Parity Tests
server.Newwrapper, MCP responses (tools/list,tools/call, resources, prompts) under a Cedar policy remain equivalent before/after — the live path still uses the HTTP authz middleware in Phase 1 (removed in P2.3 Move middleware chain under Serve; remove authz + annotation mw #5441), so this asserts the wrapper is unchanged while the new core seam is exercised directly in unit tests.Edge Cases
AuthorizeWithJWTClaimsreturnsErrMissingPrincipalwhen identity is absent from ctx — confirm the adapter always re-injects so this only surfaces for a genuinely nil identity, and that nil/anonymous identity behaves consistently between filter and call.Tool.Annotations(converter returns nil, no annotation ctx write).hasAnyHintgate).Out of Scope
server.Newpath during Phase 1.cfg.Authz/ CRD / YAML config (unchanged input).OutgoingAuthRegistry).Serve/ServerConfigor touchingserver.New's body (Phases 2-3).References