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
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.
Description
When using
MockStdioResponsePluginwithblockUnmockedRequests: true, the@stdin.body.idplaceholder 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.idinstead 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)Run
Expected behavior
Actual behavior
The first message resolves
@stdin.body.idto1correctly. 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
MockStdioResponsePluginto 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.