feat(mcp): per-client pretty-printed results via the X-MCP-Pretty header

Compact JSON stays the platform default: measured with an OpenAI-family tokenizer on realistic
payloads, 2-space indentation costs 14-46 % more tokens (2-2.7 tokens per line for newline,
indent and colon spacing) — gzip removes it from the wire, not from a model's context. A human
reading results in an interactive client opts in per client with `X-MCP-Pretty: 1`; the access
guard stores the preference on the caller context and stringifyResult honours it. This repo's
.mcp.json sets the header so operator sessions in Claude Code get readable output. The
MCP_PRETTY_JSON environment switch remains the global escape hatch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-02 03:30:50 +02:00
parent 7288220341
commit 05641d4076
7 changed files with 60 additions and 11 deletions
+4 -1
View File
@@ -2,7 +2,10 @@
"mcpServers": {
"autopilot-monitor": {
"type": "http",
"url": "https://mcp.autopilotmonitor.com/mcp"
"url": "https://mcp.autopilotmonitor.com/mcp",
"headers": {
"X-MCP-Pretty": "1"
}
},
"playwright": {
"command": "cmd",
+1 -1
Submodule docs updated: 0857248682...755cc01cf7
@@ -11,6 +11,7 @@ import { McpServer } from '@modelcontextprotocol/server';
import { registerTools } from '../tools.js';
import {
runWithCaller,
wantsPrettyJson,
DEFAULT_FIRST_PAGE_SIZE,
DEFAULT_SCAN_BUDGET,
DEFAULT_SCAN_ROW_BUDGET,
@@ -167,13 +168,31 @@ describe('first-page defaults', () => {
});
describe('result serialization', () => {
it('tool results are compact JSON (no indentation whitespace)', () => {
const payload = { sessions: [{ id: 1, tags: ['a', 'b'] }], nested: { deep: { x: null } } };
const payload = { sessions: [{ id: 1, tags: ['a', 'b'] }], nested: { deep: { x: null } } };
it('tool results are compact JSON (no indentation whitespace) by default', () => {
const text = toolResultText(payload, 1000).content[0].text;
expect(text).toBe(JSON.stringify(payload));
expect(text).not.toContain('\n');
expect(stringifyResult(payload)).toBe(text);
});
it('a caller that opted in via X-MCP-Pretty gets indented JSON for the same payload', () => {
const text = runWithCaller({ ...GA, prettyJson: true }, () => toolResultText(payload, 1000).content[0].text);
expect(text).toBe(JSON.stringify(payload, null, 2));
// The opt-in is scoped to that caller's async context — the next caller is compact again.
expect(runWithCaller(GA, () => stringifyResult(payload))).toBe(JSON.stringify(payload));
});
it('accepts "1" and "true" (any case) as the header opt-in and nothing else', () => {
expect(wantsPrettyJson('1')).toBe(true);
expect(wantsPrettyJson('true')).toBe(true);
expect(wantsPrettyJson(' TRUE ')).toBe(true);
expect(wantsPrettyJson(['1'])).toBe(true);
expect(wantsPrettyJson('0')).toBe(false);
expect(wantsPrettyJson('yes')).toBe(false);
expect(wantsPrettyJson(undefined)).toBe(false);
});
});
describe('scan budget', () => {
@@ -10,7 +10,7 @@
import type { Request, Response, NextFunction } from 'express';
import crypto from 'node:crypto';
import { extractTokenClaims, isTokenExpired } from './auth.js';
import { runWithCaller } from './client.js';
import { runWithCaller, wantsPrettyJson } from './client.js';
import { API_BASE_URL, getPublicBaseUrl, parsePositiveInt } from './config.js';
import type { CheckMcpAccessResponse } from './generated/wire-types.generated.js';
@@ -490,6 +490,9 @@ export function accessGuard(req: Request, res: Response, next: NextFunction): vo
// UPN domain labels the synthesized home-tenant entry in list_tenants for a delegated
// caller (the home tenant is never in the backend-bounded config/all subset).
upn,
// Per-client readability opt-in (see CallerContext.prettyJson): compact JSON stays the
// default for every AI agent; a human-facing client sends the header.
prettyJson: wantsPrettyJson(req.headers['x-mcp-pretty']),
},
() => next(),
);
@@ -77,10 +77,30 @@ interface CallerContext {
* in utils/homeTenantScope.ts).
*/
upn?: string;
/**
* The caller asked for indented (pretty-printed) tool results via the `X-MCP-Pretty: 1` request
* header — a per-CLIENT preference for humans reading results in an interactive client. The
* platform default is compact JSON: measured with an OpenAI-family tokenizer, indentation costs
* 14-46 % more tokens (2-2.7 tokens per line for newline + indent + colon spacing), which the
* wire's gzip does not remove from a model's context.
*/
prettyJson?: boolean;
}
const callerStore = new AsyncLocalStorage<CallerContext>();
/** True for the accepted `X-MCP-Pretty` header values ("1" / "true", case-insensitive). */
export function wantsPrettyJson(headerValue: string | string[] | undefined): boolean {
const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;
const v = raw?.trim().toLowerCase();
return v === '1' || v === 'true';
}
/** True when the current caller opted into pretty-printed results (see CallerContext.prettyJson). */
export function isPrettyJsonRequested(): boolean {
return callerStore.getStore()?.prettyJson === true;
}
/**
* Run a callback within an async context that carries the given caller info.
* All calls to `apiFetch` and routing helpers inside the callback (and its
@@ -35,10 +35,12 @@ export const ENTRA_LOGIN_BASE_URL = 'https://login.microsoftonline.com';
export const DOCS_BASE_URL = 'https://docs.autopilotmonitor.com';
/**
* Pretty-print tool results (2-space indentation) instead of compact JSON. Off by default:
* Pretty-print EVERY tool result (2-space indentation) instead of compact JSON. Off by default:
* the consumer of a tool result is a language model whose context window is not gzipped —
* measured on the static catalogs, indentation added 18–55 % characters per response for
* zero information. Set MCP_PRETTY_JSON=true for a human eyeballing results in a debugger.
* measured with an OpenAI-family tokenizer, indentation adds 14–46 % tokens per response for
* zero information. Prefer the per-client `X-MCP-Pretty: 1` request header (client.ts
* wantsPrettyJson) so only the humans who read results pay for them; this global switch is
* the debugger escape hatch.
*/
export const PRETTY_JSON = process.env.MCP_PRETTY_JSON?.trim().toLowerCase() === 'true';
@@ -1,6 +1,7 @@
import type { ToolAnnotations } from '@modelcontextprotocol/server';
import { z } from 'zod';
import { PRETTY_JSON } from '../config.js';
import { isPrettyJsonRequested } from '../client.js';
/**
* Projection sent by get_session_events when the caller omits `fields`: everything an event
@@ -196,11 +197,12 @@ export const MAX_RESULT_SIZE_CHARS = {
/**
* Serializes a tool result or resource body. Compact by default: the text lands verbatim in
* a language model's context, which — unlike the gzipped wire — pays for every indentation
* space (measured +18–55 % characters on the static catalogs). MCP_PRETTY_JSON=true restores
* the indented form for humans reading results in a debugger.
* character (measured with an OpenAI-family tokenizer: +14–46 % tokens, 2–2.7 tokens per line).
* Indented output is available per client via the `X-MCP-Pretty: 1` request header (humans
* reading results in an interactive client) or globally via MCP_PRETTY_JSON=true.
*/
export function stringifyResult(data: unknown): string {
return PRETTY_JSON ? JSON.stringify(data, null, 2) : JSON.stringify(data);
return PRETTY_JSON || isPrettyJsonRequested() ? JSON.stringify(data, null, 2) : JSON.stringify(data);
}
/**