[v2] MRTR wipe-cache example reads unvalidated elicitation content and defaults malformed scope to all
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Documentation
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- documentation, security
Research direction
Open docs/servers/input-required.md and locate the “Carry state across rounds with requestState” wipe-cache example. Read the schema-aware acceptedContent() usage earlier on the page, then update both confirmation and scope handling to validate client content at runtime. Done means invalid or missing scope requests input again instead of defaulting to “all”.
Written by the indexing model from the issue text.
Description
What happened?
The v2 input_required documentation correctly states that
ctx.mcpReq.inputResponses is client-provided and untrusted, and recommends
using the schema-aware overload of acceptedContent().
However, the sequential wipe-cache example uses the unchecked generic
overload for both confirmation and scope:
const confirmed = acceptedContent<{ confirm: boolean }>(
ctx.mcpReq.inputResponses,
'confirm'
);
It later treats that response as proof that the operator confirmed:
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: 'Really wipe the cache?',
requestedSchema: {
type: 'object',
properties: {
confirm: {
type: 'boolean'
}
},
required: ['confirm']
}
})
}
});
}
return inputRequired({
inputRequests: {
scope: inputRequired.elicit({
message: 'Which scope?',
requestedSchema: {
type: 'object',
properties: {
scope: {
type: 'string'
}
},
required: ['scope']
}
})
},
requestState: await stateCodec.mint({
step: 'confirmed'
})
});
The generic type parameter only affects TypeScript typing. It does not perform
runtime validation of the client-provided value.
For example, a malformed accepted response could contain:
{
"action": "accept",
"content": {
"confirm": "false"
}
}
The string "false" is truthy in JavaScript. Because the example only checks:
confirmed?.confirm !== true
this particular value does not equal the boolean true, so the current
!== true check fails closed.
However, the example still demonstrates reading untrusted client content using
a compile-time generic instead of the runtime-validating overload. A copied or
slightly modified version using a normal truthiness check such as:
if (!confirmed?.confirm) {
// request confirmation
}
would treat "false" as confirmation.
The same example later reads the requested scope with the unchecked overload:
const scope = acceptedContent<{ scope: string }>(
ctx.mcpReq.inputResponses,
'scope'
);
return {
content: [{
type: 'text',
text: `Wiped ${scope?.scope ?? 'all'}`
}]
};
If the scope response is missing, malformed, or of an unexpected type, the
example falls back to "all". That feels unsafe for a destructive example and
conflicts with the page's earlier guidance to validate inputResponses using
the schema-aware overload.
This is primarily a documentation and defensive API-usage issue, not a claim
that a malicious MCP client gains a capability it did not already possess.
What did you expect?
I expected the destructive wipe-cache example to follow the page's own
guidance and validate all client-provided elicitation content at runtime.
For example:
const confirmationSchema = z.object({
confirm: z.boolean()
});
const scopeSchema = z.object({
scope: z.string()
});
const confirmed = acceptedContent(
ctx.mcpReq.inputResponses,
'confirm',
confirmationSchema
);
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: 'Really wipe the cache?',
requestedSchema: confirmationSchema
})
}
});
}
const scope = acceptedContent(
ctx.mcpReq.inputResponses,
'scope',
scopeSchema
);
if (scope === undefined) {
return inputRequired({
inputRequests: {
scope: inputRequired.elicit({
message: 'Which scope?',
requestedSchema: scopeSchema
})
}
});
}
return {
content: [{
type: 'text',
text: `Wiped ${scope.scope}`
}]
};
The example should also fail closed when scope content is missing or invalid,
rather than defaulting to "all".
Code to reproduce
Documentation page:
`docs/servers/input-required.md`
Relevant section:
`Carry state across rounds with requestState`
Relevant example:
const confirmed = acceptedContent<{ confirm: boolean }>(
ctx.mcpReq.inputResponses,
'confirm'
);
const scope = acceptedContent<{ scope: string }>(
ctx.mcpReq.inputResponses,
'scope'
);
return {
content: [{
type: 'text',
text: `Wiped ${scope?.scope ?? 'all'}`
}]
};
Minimal demonstration that the generic type does not validate at runtime:
const clientContent: unknown = {
confirm: 'false'
};
const confirmed =
clientContent as {
confirm: boolean;
};
console.log(typeof confirmed.confirm);
// "string"
console.log(Boolean(confirmed.confirm));
// true
The cast changes the TypeScript type but does not transform or validate the
runtime value.
SDK version
main at commit 1e1392e3f91583884fe82a0b4b91335875c3fba6
Area
Documentation
- Dominant language
- TypeScript
- Stars
- 13.5k
- Forks
- 2.3k
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 55
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/typescript-sdk
-
v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
modelcontextprotocol/typescript-sdk#2946 ·
Maintainers usually reply within 1 day
-
[v2] URI template reserved expansions encode existing %HH sequences againPossibly taken @takagibit18 claimed this 4 days ago. Openv1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
modelcontextprotocol/typescript-sdk#2920 · 1 comment ·
Maintainers usually reply within 1 day
-
[v2] URI template strict expansions leave !'()* unencodedPossibly taken @takagibit18 claimed this 4 days ago. Openv1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
modelcontextprotocol/typescript-sdk#2919 · 1 comment ·
Maintainers usually reply within 1 day
-
Malformed params on spec request methods return -32603 Internal error instead of -32602 Invalid paramsPossibly taken A pull request linked to this issue is open or already merged. Openv1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
modelcontextprotocol/typescript-sdk#2916 · 3 comments ·
Maintainers usually reply within 1 day
-
Unconditional `prompt=consent` (when `offline_access` in scope) blocks OAuth in Entra tenants with user consent disabled + admin consent grantedPossibly taken @dasjideepak claimed this 10 days ago. Openv1 v2
Difficulty 1/5 Under an hour Newbie friendliness 90/100
modelcontextprotocol/typescript-sdk#2867 · 1 comment ·
Maintainers usually reply within 1 day
All issues in modelcontextprotocol/typescript-sdk
Similar issues
-
bot:ai-assisted status:untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
midnightntwrk/midnight-js#1424 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
mksglu/context-mode#1268 ·
Maintainers usually reply within 5 days
-
[bug] Setup fails with "Cannot find matching keyid" when an older Node's corepack is on PATHPossibly taken @EyalPoly claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
MystenLabs/MemWal#1124 · 2 comments ·
Maintainers usually reply within 1 day
-
Edit:Opencheck:failed streams:edit
Difficulty 2/5 1-3 hours Newbie friendliness 60/100
iptv-org/iptv#54352 · 1 comment ·
Maintainers usually reply within 1 day