MockStdioResponsePlugin: @stdin.body.id placeholder fails to resolve when messages arrive back-to-back after an id-less message

Open
#1,757 0 comments 1 reaction 2 assignees View on GitHub

@garrytrinder is already working on this.

Since Jul 13, 2026.

  • #1758 by @copilot-swe-agent — open

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/dev-proxy

All issues in dotnet/dev-proxy

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.