fix(mcp): preserve confirmation compatibility across clients

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
MaheshtheDev 2026-10-14 21:11:48 +00:00
parent d700fc1fad
commit d797bf249f
5 changed files with 9 additions and 15 deletions

View file

@ -70,7 +70,6 @@ function resourceMetadata(c: Context<{ Bindings: Bindings }>) {
const canonicalMetadataUrl = new URL(
protectedResourceMetadataUrl(mcpResource),
)
// MCP clients also try the root well-known location as a compatibility fallback.
const isMcpRootFallback =
requestUrl.pathname === PROTECTED_RESOURCE_METADATA_PATH &&
requestUrl.search === ""

View file

@ -1,13 +1,7 @@
export const PROTECTED_RESOURCE_METADATA_PATH =
"/.well-known/oauth-protected-resource"
/**
* Builds the protected-resource metadata URL advertised to OAuth clients.
*
* The canonical resource is the single source of truth so the advertised URL
* and the metadata document cannot disagree. Forwarded headers are intentionally
* excluded because they are request-controlled in some proxy deployments.
*/
// Discovery must use the configured resource, never request-controlled forwarding headers.
export function protectedResourceMetadataUrl(resource: string): string {
const resourceUrl = new URL(resource)
const resourcePath = resourceUrl.pathname === "/" ? "" : resourceUrl.pathname

View file

@ -10,7 +10,6 @@ import { forgetMemory } from "./forget-memory"
import { addMemoryOutputSchema, type AddMemoryOutput } from "./output-schemas"
import { textContent, type ToolDeps } from "./types"
// v5 metadata values are flat: string, number, boolean or string list.
export const memoryMetadataSchema = z
.record(
z.string(),

View file

@ -28,15 +28,18 @@ export interface ForgetMemoryArgs {
containerTag?: string
}
// Only 2026-era requests carry capabilities per request; stateless legacy requests never saw initialize, so they use the confirm flag instead.
// Stateless legacy requests cannot receive elicitation, so they use explicit two-call confirmation.
function canElicit(context: ServerContext | undefined): boolean {
const envelope = context?.mcpReq?.envelope
if (!envelope) return false
const capabilities = Reflect.get(envelope, CLIENT_CAPABILITIES_META_KEY)
if (!capabilities || typeof capabilities !== "object") return false
const elicitation = Reflect.get(capabilities, "elicitation")
return Boolean(
capabilities &&
typeof capabilities === "object" &&
Reflect.get(capabilities, "elicitation"),
elicitation &&
typeof elicitation === "object" &&
(Reflect.get(elicitation, "form") !== undefined ||
Reflect.get(elicitation, "url") === undefined),
)
}
@ -60,7 +63,6 @@ function askToConfirm(
},
}),
},
// Only compared against the server-resolved ID, so a tampered value can just trigger a re-ask.
requestState: candidate.id,
})
}

View file

@ -25,13 +25,13 @@ export const addMemoryOutputSchema = z.object({
message: z.string(),
id: z.string().optional(),
status: z.string().optional(),
memory: z.string().optional(),
})
export type AddMemoryOutput = z.infer<typeof addMemoryOutputSchema>
export const forgetMemoryOutputSchema = addMemoryOutputSchema.extend({
action: z.literal("forget"),
memory: z.string().optional(),
})
export const getDocumentOutputSchema = z.object({