microsoft/vscode-languageserver-node

Message middleware / interceptor API for `vscode-jsonrpc`

Open

#1,757 opened on Apr 28, 2026

View on GitHub
 (3 comments) (0 reactions) (0 assignees)TypeScript (397 forks)auto 404
feature-requesthelp wanted

Repository metrics

Stars
 (1,776 stars)
PR merge metrics
 (PR metrics pending)

Description

Feature Request: Message middleware / interceptor API for vscode-jsonrpc

Summary

Add a way to intercept or decorate outgoing and incoming JSON-RPC messages at the transport layer, so that callers can inject or extract arbitrary metadata (such as W3C Trace Context traceparent/tracestate headers) without forking the library.

Motivation

vscode-jsonrpc is used as the JS/TS transport in many language server and extension host scenarios. In our scenario, we are communicating between a VS Code extension and a .NET process. That process uses StreamJsonRpc, which offers an ActivityTracingStrategy that automatically injects W3C Trace Context into outgoing MessagePack envelopes and extracts it from incoming ones, enabling distributed tracing across the process boundary.

We would like the same on the JS side. Concretely: when a TS client sends a JSON-RPC request, we want to inject a traceparent field alongside jsonrpc, id, method, and params. StreamJsonRpc will then pick it up and parent the server-side span under the client's span, giving a cohesive cross-process trace in tools like Jaeger or the Aspire Dashboard.

Currently this is not possible because MessageWriter is constructed inside createMessageConnection() and is not accessible to callers after the fact. There is no hook to wrap or intercept messages before they are written to the stream.

Proposed API

A message middleware interface that can be passed to createMessageConnection():

export interface MessageMiddleware {
  /** Called before a message is written. Return the (optionally mutated) message, or a new one. */
  onSend?(message: Message): Message
  /** Called after a message is read, before it is dispatched. Return the (optionally mutated) message. */
  onReceive?(message: Message): Message
}

Usage:

import { createMessageConnection, MessageMiddleware } from 'vscode-jsonrpc'

const tracing: MessageMiddleware = {
  onSend(msg) {
    if (isRequestMessage(msg)) {
      // inject W3C traceparent alongside standard JSON-RPC fields
      ;(msg as any).traceparent = getCurrentTraceparent()
    }
    return msg
  },
  onReceive(msg) {
    if (isRequestMessage(msg)) {
      const tp = (msg as any).traceparent
      if (tp) setIncomingTraceparent(tp)
    }
    return msg
  },
}

const conn = createMessageConnection(reader, writer, logger, { middleware: tracing })

Alternatives considered

  • Subclassing MessageWriter — the writer instance is not exposed after createMessageConnection() returns, so this is not possible without forking.
  • Using a custom StreamMessageWriter wrapper — requires re-implementing the full framing protocol; fragile and error-prone.
  • Encoding metadata in method params — invasive; requires changes to every call site; breaks protocol contracts.

Notes

  • The traceparent field is a top-level JSON-RPC extension field (not inside params), which is valid per the JSON-RPC 2.0 spec (unknown fields are ignored). StreamJsonRpc already uses this convention.
  • This request is not limited to tracing — the same API would be useful for propagating auth tokens, request deadlines, or any other cross-cutting concern.

Contributor guide