MockStdioResponsePlugin: @stdin.body.id placeholder fails to resolve when messages arrive back-to-back after an id-less message
Assessment
This issue has not been assessed yet.
Description
Description
When using MockStdioResponsePlugin with blockUnmockedRequests: true, the @stdin.body.id placeholder stops resolving correctly for messages that immediately follow an id-less JSON-RPC notification (e.g. notifications/initialized). The placeholder is output as the literal text @stdin.body.id instead of the numeric id from the current message.
Steps to reproduce
Dev Proxy version: 3.1.0
OS: macOS (Apple Silicon)
Config (.devproxy/devproxyrc.json)
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.1.0/rc.schema.json",
"plugins": [
{
"name": "MockStdioResponsePlugin",
"enabled": true,
"pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
"configSection": "mockStdioResponsePlugin"
}
],
"mockStdioResponsePlugin": {
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.1.0/mockstdioresponseplugin.schema.json",
"mocksFile": "stdio-mocks.json",
"blockUnmockedRequests": true
}
}
Mocks (.devproxy/stdio-mocks.json)
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.1.0/mockstdioresponseplugin.mocksfile.schema.json",
"mocks": [
{
"request": { "bodyFragment": "protocolVersion" },
"response": {
"stdout": "{\"jsonrpc\":\"2.0\",\"id\":@stdin.body.id,\"result\":{\"protocolVersion\":\"2024-11-05\"}}\n"
}
},
{
"request": { "bodyFragment": "accept_eula" },
"response": {
"stdout": "{\"jsonrpc\":\"2.0\",\"id\":@stdin.body.id,\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"ok\"}]}}\n"
}
}
]
}
Repro script (repro.mjs)
import { spawn } from "node:child_process";
const proc = spawn("devproxy", ["stdio", "--config-file", ".devproxy/devproxyrc.json", "--", "cat"], {
stdio: ["pipe", "pipe", "pipe"],
});
let buf = "";
proc.stdout.on("data", (c) => {
buf += c;
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i).trim();
buf = buf.slice(i + 1);
if (!line) continue;
console.log("← stdout:", line);
}
});
// 1. Send initialize (has id) — works fine
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05" } }) + "\n");
// 2. Wait for response, then send notification + accept_eula back-to-back
setTimeout(() => {
// Fire-and-forget notification (no id field)
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
// Immediately followed by a request with id
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "accept_eula", arguments: {} } }) + "\n");
}, 500);
setTimeout(() => {
proc.kill();
process.exit(0);
}, 3000);
Run
node repro.mjs
Expected behavior
← stdout: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05"}}
← stdout: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ok"}]}}
Actual behavior
← stdout: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05"}}
← stdout: {"jsonrpc":"2.0","id":@stdin.body.id,"result":{"content":[{"type":"text","text":"ok"}]}}
The first message resolves @stdin.body.id to 1 correctly. The second message (after the id-less notification) outputs the literal placeholder text.
Workaround
Adding a delay (e.g. 100ms) between the notification and the subsequent request makes it work — suggesting the issue is related to how messages are buffered/parsed when they arrive in the same I/O read.
Context
This blocks using MockStdioResponsePlugin to mock MCP servers that follow the standard JSON-RPC handshake (initialize → notifications/initialized → tools/call), where the notification and first tool call are sent synchronously in the same callback.
- Dominant language
- C#
- Stars
- 832
- Forks
- 89
- Avg merge
- 12h 1m
- Merged PRs (30d)
- 23
Contributor 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 dotnet/dev-proxy
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
-
needs peer review
Difficulty 3/5 1-2 days Newbie friendliness 68/100
All issues in dotnet/dev-proxy
Similar issues
-
type/automation type/tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
t/bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
ci-failure-cause test-failure
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
area:auth FE mvp P3
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
klasolsson81/jobbliggaren#1788 ·