Skip to content

Commit 6f68085

Browse files
jirispilkaclaude
andauthored
refactor: Extract shared injectMcpSessionId helper (#1074)
Part of #1066 (checkbox: shared `injectMcpSessionId` helper). ## What we're solving The mcpSessionId injection — `params._meta ??= {}; params._meta.mcpSessionId = <id>` — was duplicated in `src/stdio.ts` and `src/dev_server.ts`. The "always create params" invariant, and the comment explaining why (requests like `listTasks`/`getTasks` have no params; skipping creation makes injection silently fail and breaks multi-node session isolation), lived only in stdio.ts. ## How Add `injectMcpSessionId(params, mcpSessionId)` to `src/utils/mcp.ts`, carrying the invariant and its comment once. Both entry points call it. A unit test locks the always-create invariant and the mutate-and-return semantics. - **stdio.ts**: behavior unchanged (it already always-created params). - **dev_server.ts**: now *also* always-creates params via the helper. Previously it injected only when `req.body.params` already existed (`if (req.body?.params && sessionId)`); it now injects whenever there's a session (`if (sessionId && req.body)`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016pXZvhEoE6Lz3DpBSL3n1Y --- _Generated by [Claude Code](https://claude.ai/code/session_016pXZvhEoE6Lz3DpBSL3n1Y)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 34c2d81 commit 6f68085

4 files changed

Lines changed: 58 additions & 10 deletions

File tree

src/dev_server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { parseBooleanOrNull } from '@apify/utilities';
1616
import { ApifyClient } from './apify_client.js';
1717
import { ActorsMcpServer } from './mcp/server.js';
1818
import { resolvePaymentProvider } from './payments/index.js';
19+
import { injectMcpSessionId } from './utils/mcp.js';
1920
import { parseServerMode } from './utils/server_mode.js';
2021

2122
// DEV ONLY. This is a local dev/standby-emulation server, not the hosted HTTP server.
@@ -208,9 +209,8 @@ export function createExpressApp(): express.Express {
208209
}
209210

210211
// Inject session ID into request params for the reused existing session
211-
if (req.body?.params && sessionId) {
212-
req.body.params._meta ??= {};
213-
req.body.params._meta.mcpSessionId = sessionId;
212+
if (sessionId && req.body) {
213+
req.body.params = injectMcpSessionId(req.body.params, sessionId);
214214
}
215215

216216
// Handle the request with existing transport - no need to reconnect

src/stdio.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { getTelemetryEnv } from './telemetry.js';
3838
import type { ApifyRequestParams, Input, ServerModeOption, TelemetryEnv, ToolSelector } from './types.js';
3939
import { isApiTokenRequired } from './utils/auth.js';
4040
import { parseCommaSeparatedList } from './utils/generic.js';
41+
import { injectMcpSessionId } from './utils/mcp.js';
4142
import { parseServerMode } from './utils/server_mode.js';
4243
import { getPackageVersion } from './utils/version.js';
4344

@@ -239,12 +240,7 @@ async function main() {
239240
transport.onmessage = (message) => {
240241
const msgRecord = message as Record<string, unknown>;
241242
// Inject session ID into all requests for task isolation and session tracking.
242-
// CRITICAL: Always create params object if missing (some requests like listTasks/getTasks don't have params),
243-
// otherwise mcpSessionId injection fails, breaking session isolation in multi-node setups.
244-
const params = (msgRecord.params || {}) as ApifyRequestParams;
245-
params._meta ??= {};
246-
params._meta.mcpSessionId = mcpSessionId;
247-
msgRecord.params = params;
243+
msgRecord.params = injectMcpSessionId(msgRecord.params as ApifyRequestParams | undefined, mcpSessionId);
248244

249245
sdkOnMessage?.(message);
250246
};

src/utils/mcp.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CallToolResult, ContentBlock } from '@modelcontextprotocol/sdk/types.js';
22

33
import { FAILURE_CATEGORY, TOOL_STATUS } from '../const.js';
4-
import type { AjvErrorDetails, FailureCategory, ToolTelemetryContext } from '../types.js';
4+
import type { AjvErrorDetails, ApifyRequestParams, FailureCategory, ToolTelemetryContext } from '../types.js';
55
import { ACTOR_RUN_LIMIT_MESSAGE, isActorRunLimitError } from './apify_errors.js';
66
import { wrapJsonText } from './encode_text.js';
77
import { getHttpStatusCode } from './logging.js';
@@ -10,6 +10,21 @@ import { classifyFailureCategory, getToolStatusFromError } from './tool_status.j
1010
/** MCP `_meta` key for Apify Actor run information. Namespaced per MCP spec. */
1111
export const APIFY_ACTOR_RUN_META_KEY = 'com.apify/ActorRun';
1212

13+
/**
14+
* Injects the MCP session ID into request parameters.
15+
* Always ensures a params object exists, even for requests that normally have no params (e.g., listTasks/getTasks),
16+
* otherwise mcpSessionId injection fails, breaking session isolation in multi-node setups.
17+
* @param params Request parameters (may be undefined)
18+
* @param mcpSessionId Session ID to inject
19+
* @returns Params object with _meta.mcpSessionId set
20+
*/
21+
export function injectMcpSessionId(params: ApifyRequestParams | undefined, mcpSessionId: string): ApifyRequestParams {
22+
const result = (params || {}) as ApifyRequestParams;
23+
result._meta ??= {};
24+
result._meta.mcpSessionId = mcpSessionId;
25+
return result;
26+
}
27+
1328
/**
1429
* Builds usage metadata for MCP response from a source object containing Apify run costs.
1530
* Nests fields under the `com.apify/ActorRun` namespaced key as required by the MCP `_meta` spec

tests/unit/utils.mcp.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { wrapJsonText } from '../../src/utils/encode_text.js';
88
import {
99
computeToolResponseBytes,
1010
getToolCallErrorUserText,
11+
injectMcpSessionId,
1112
respondAborted,
1213
respondErrorNoTelemetry,
1314
respondJson,
@@ -19,6 +20,42 @@ import {
1920
} from '../../src/utils/mcp.js';
2021
import { textOf } from './helpers/tool_context.js';
2122

23+
describe('injectMcpSessionId()', () => {
24+
it('returns a new params object with _meta.mcpSessionId when params is undefined', () => {
25+
const result = injectMcpSessionId(undefined, 'session-123');
26+
expect(result).toEqual({
27+
_meta: { mcpSessionId: 'session-123' },
28+
});
29+
expect(typeof result).toBe('object');
30+
});
31+
32+
it('mutates and returns the same object when params exists without _meta', () => {
33+
const input = { foo: 'bar' };
34+
const result = injectMcpSessionId(input, 'session-456');
35+
expect(result === input).toBe(true);
36+
expect(result).toEqual({
37+
foo: 'bar',
38+
_meta: { mcpSessionId: 'session-456' },
39+
});
40+
});
41+
42+
it('preserves other _meta fields and overwrites an existing mcpSessionId', () => {
43+
const input = {
44+
someParam: 'value',
45+
_meta: {
46+
apifyToken: 'token-abc',
47+
mcpSessionId: 'old-session',
48+
},
49+
};
50+
const result = injectMcpSessionId(input, 'session-789');
51+
expect(result === input).toBe(true);
52+
expect(result._meta).toEqual({
53+
apifyToken: 'token-abc',
54+
mcpSessionId: 'session-789',
55+
});
56+
});
57+
});
58+
2259
describe('respondOk()', () => {
2360
it('returns a success response with the raw text and no isError/telemetry', () => {
2461
const result = respondOk('all good');

0 commit comments

Comments
 (0)