Skip to content

Commit 164fe67

Browse files
committed
fix(parallel): cap free Search MCP session_id at its 100-char tools/list contract
The free parallel-free provider reused the paid ParallelSearchSchema, whose session_id allows 1000 chars, but the live Search MCP tools/list schema caps session_id at 100. Parameterize normalizeParallelSessionId(value, maxLength); the free path passes 100 (paid keeps 1000) and advertises the tighter bound in its own ParallelFreeSearchSchema. An over-limit caller id is dropped and a fresh in-contract id is minted. Updates tests and docs accordingly.
1 parent 56ac306 commit 164fe67

8 files changed

Lines changed: 87 additions & 17 deletions

docs/tools/parallel-search.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,11 @@ Results to return (1-40).
110110
</ParamField>
111111

112112
<ParamField path="session_id" type="string">
113-
Optional Parallel session id (max 1000 chars). Pass the `sessionId` from a
114-
previous Parallel result on follow-up searches that are part of the same task
115-
so Parallel can group related calls and improve subsequent results.
113+
Optional Parallel session id (max 1000 chars on `parallel`; the free
114+
`parallel-free` Search MCP caps it at 100). Pass the `sessionId` from a previous
115+
Parallel result on follow-up searches that are part of the same task so Parallel
116+
can group related calls and improve subsequent results. An id past the limit is
117+
dropped and a fresh one is generated.
116118
</ParamField>
117119

118120
<ParamField path="client_model" type="string">

extensions/parallel/src/parallel-free-web-search-provider.runtime.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
normalizeParallelObjective,
2121
normalizeParallelSearchQueries,
2222
normalizeParallelSessionId,
23+
PARALLEL_FREE_SESSION_ID_MAX_LENGTH,
2324
resolveParallelSearchCount,
2425
stripParallelGeneratedSessionId,
2526
} from "./parallel-search-normalize.js";
@@ -50,7 +51,10 @@ export async function executeParallelFreeWebSearchProviderTool(
5051
readNumberParam(args, "count", { integer: true }) ??
5152
(typeof searchConfig?.maxResults === "number" ? searchConfig.maxResults : undefined);
5253
const count = resolveParallelSearchCount(requestedCount ?? DEFAULT_SEARCH_COUNT);
53-
const sessionId = normalizeParallelSessionId(readStringParam(args, "session_id"));
54+
const sessionId = normalizeParallelSessionId(
55+
readStringParam(args, "session_id"),
56+
PARALLEL_FREE_SESSION_ID_MAX_LENGTH,
57+
);
5458
const clientModel = normalizeParallelClientModel(readStringParam(args, "client_model"));
5559
const cacheKey = buildParallelCacheKey({
5660
endpoint: PARALLEL_MCP_SEARCH_URL,

extensions/parallel/src/parallel-free-web-search-provider.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ describe("parallel-free web search provider", () => {
7171
expect(provider.autoDetectOrder).toBe(76);
7272
});
7373

74+
it("advertises the free MCP's tighter 100-char session_id cap in its tool schema", () => {
75+
const provider = createParallelFreeWebSearchProvider();
76+
const tool = provider.createTool({ config: {}, searchConfig: {} });
77+
if (!tool) {
78+
throw new Error("Expected tool definition");
79+
}
80+
const sessionIdParam = (
81+
tool.parameters as { properties: Record<string, { maxLength?: number }> }
82+
).properties.session_id;
83+
expect(sessionIdParam.maxLength).toBe(100);
84+
});
85+
7486
it("searches via the free MCP and brands the result, with no API key", async () => {
7587
// No PARALLEL_API_KEY needed — the free path ignores keys entirely.
7688
vi.stubEnv("PARALLEL_API_KEY", "par-should-be-ignored"); // pragma: allowlist secret
@@ -108,6 +120,29 @@ describe("parallel-free web search provider", () => {
108120
vi.unstubAllEnvs();
109121
});
110122

123+
it("drops an over-limit caller session id and mints one within the free MCP's 100-char cap", async () => {
124+
pushHandshake({ search_id: "s1", results: [] });
125+
const provider = createParallelFreeWebSearchProvider();
126+
const tool = provider.createTool({ config: {}, searchConfig: {} });
127+
if (!tool) {
128+
throw new Error("Expected tool definition");
129+
}
130+
await tool.execute({
131+
objective: "session cap check",
132+
search_queries: ["session cap"],
133+
session_id: "x".repeat(150),
134+
});
135+
136+
const toolsCallArgs = (
137+
JSON.parse(endpointMockState.calls[2].init.body as string).params as Record<string, unknown>
138+
).arguments as Record<string, unknown>;
139+
const sentSessionId = toolsCallArgs.session_id as string;
140+
// The 150-char caller id is out-of-contract for the free MCP; it is dropped
141+
// and replaced by a generated id that stays within the advertised 100-char cap.
142+
expect(sentSessionId).not.toBe("x".repeat(150));
143+
expect(sentSessionId.length).toBeLessThanOrEqual(100);
144+
});
145+
111146
it("returns a structured error when search_queries is missing", async () => {
112147
const provider = createParallelFreeWebSearchProvider();
113148
const tool = provider.createTool({ config: {}, searchConfig: {} });

extensions/parallel/src/parallel-free-web-search-provider.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract";
22
import { createParallelFreeWebSearchProviderBase } from "./parallel-free-web-search-provider.shared.js";
3+
import { PARALLEL_FREE_SESSION_ID_MAX_LENGTH } from "./parallel-search-normalize.js";
34
// Reuse the paid provider's tool schema — both transports accept the same
4-
// objective + search_queries shape; only the description/runtime differ.
5+
// objective + search_queries shape — but the free Search MCP caps session_id at
6+
// 100 chars (its `tools/list` schema), tighter than the paid v1 REST limit, so
7+
// the free model-facing schema advertises that tighter bound.
58
import { ParallelSearchSchema } from "./parallel-web-search-provider.js";
69

10+
const ParallelFreeSearchSchema = {
11+
...ParallelSearchSchema,
12+
properties: {
13+
...ParallelSearchSchema.properties,
14+
session_id: {
15+
...ParallelSearchSchema.properties.session_id,
16+
maxLength: PARALLEL_FREE_SESSION_ID_MAX_LENGTH,
17+
},
18+
},
19+
} satisfies Record<string, unknown>;
20+
721
type ParallelFreeWebSearchRuntime = typeof import("./parallel-free-web-search-provider.runtime.js");
822

923
let parallelFreeWebSearchRuntimePromise: Promise<ParallelFreeWebSearchRuntime> | undefined;
@@ -19,7 +33,7 @@ export function createParallelFreeWebSearchProvider(): WebSearchProviderPlugin {
1933
createTool: (ctx) => ({
2034
description:
2135
"Search the web using Parallel's free Search MCP (no API key). Returns ranked, LLM-optimized dense excerpts from web sources. Pass an `objective` describing the underlying question along with 2-3 short keyword `search_queries` (Parallel's recommended pairing). For multi-step research, thread the prior result's `sessionId` back in as `session_id` to keep Parallel's context grouped.",
22-
parameters: ParallelSearchSchema,
36+
parameters: ParallelFreeSearchSchema,
2337
execute: async (args, context) => {
2438
const { executeParallelFreeWebSearchProviderTool } =
2539
await loadParallelFreeWebSearchRuntime();

extensions/parallel/src/parallel-mcp-search.runtime.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,10 @@ describe("runParallelMcpSearch", () => {
245245
result: { content: [{ type: "text", text: JSON.stringify({ results: [] }) }] },
246246
}),
247247
);
248-
// A long caller id (>100 chars) accepted by the tool schema must be used as
248+
// The MCP client is a dumb transport: an already-normalized caller id (the
249+
// provider runtime caps it at the free MCP's 100-char limit) is forwarded as
249250
// sent, so the MCP session, cache key, and reported id stay in agreement.
250-
const callerSessionId = "s".repeat(150);
251+
const callerSessionId = `sess-${"a".repeat(40)}`;
251252
const response = await runParallelMcpSearch({
252253
searchQueries: ["x"],
253254
maxResults: 5,

extensions/parallel/src/parallel-search-normalize.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ const PARALLEL_MAX_SEARCH_COUNT = 40;
1717
const PARALLEL_MAX_SEARCH_QUERY_CHARS = 200;
1818
const PARALLEL_MAX_OBJECTIVE_CHARS = 5000;
1919
const PARALLEL_MAX_SEARCH_QUERIES = 5;
20-
const PARALLEL_SESSION_ID_MAX_LENGTH = 1000;
20+
// Paid v1 REST accepts session ids up to 1000 chars, but the free Search MCP
21+
// `tools/list` schema caps session_id at 100. Each runtime passes its own limit
22+
// (and advertises it in the tool schema) so callers never send an out-of-contract id.
23+
export const PARALLEL_SESSION_ID_MAX_LENGTH = 1000;
24+
export const PARALLEL_FREE_SESSION_ID_MAX_LENGTH = 100;
2125
const PARALLEL_CLIENT_MODEL_MAX_LENGTH = 100;
2226

2327
export type ParallelSearchResult = {
@@ -39,9 +43,12 @@ export function resolveParallelSearchCount(value: number): number {
3943
return Math.max(1, Math.min(PARALLEL_MAX_SEARCH_COUNT, Math.floor(value)));
4044
}
4145

42-
export function normalizeParallelSessionId(value: string | undefined): string | undefined {
46+
export function normalizeParallelSessionId(
47+
value: string | undefined,
48+
maxLength: number,
49+
): string | undefined {
4350
const trimmed = normalizeOptionalString(value);
44-
return trimmed && trimmed.length <= PARALLEL_SESSION_ID_MAX_LENGTH ? trimmed : undefined;
51+
return trimmed && trimmed.length <= maxLength ? trimmed : undefined;
4552
}
4653

4754
export function normalizeParallelObjective(value: string | undefined): string | undefined {

extensions/parallel/src/parallel-web-search-provider.runtime.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
normalizeParallelResults,
2727
normalizeParallelSearchQueries,
2828
normalizeParallelSessionId,
29+
PARALLEL_SESSION_ID_MAX_LENGTH,
2930
type ParallelSearchResponse,
3031
resolveParallelSearchCount,
3132
stripParallelGeneratedSessionId,
@@ -197,7 +198,10 @@ export async function executeParallelWebSearchProviderTool(
197198
// Always pass max_results so Parallel matches the openclaw web_search default
198199
// of 5 instead of Parallel's own default of 10.
199200
const count = resolveParallelSearchCount(requestedCount ?? DEFAULT_SEARCH_COUNT);
200-
const sessionId = normalizeParallelSessionId(readStringParam(args, "session_id"));
201+
const sessionId = normalizeParallelSessionId(
202+
readStringParam(args, "session_id"),
203+
PARALLEL_SESSION_ID_MAX_LENGTH,
204+
);
201205
const clientModel = normalizeParallelClientModel(readStringParam(args, "client_model"));
202206
const cacheKey = buildParallelCacheKey({
203207
endpoint,

extensions/parallel/src/parallel-web-search-provider.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,14 @@ describe("parallel web search provider", () => {
251251
expect(testing.normalizeParallelSearchQueries(six)).toEqual(["a", "b", "c", "d", "e"]);
252252
});
253253

254-
it("normalizes session ids, rejecting blanks and overlong values", () => {
255-
expect(testing.normalizeParallelSessionId("session-abc")).toBe("session-abc");
256-
expect(testing.normalizeParallelSessionId(" ")).toBeUndefined();
257-
expect(testing.normalizeParallelSessionId(undefined)).toBeUndefined();
258-
expect(testing.normalizeParallelSessionId("x".repeat(1001))).toBeUndefined();
254+
it("normalizes session ids, rejecting blanks and values past the given limit", () => {
255+
expect(testing.normalizeParallelSessionId("session-abc", 1000)).toBe("session-abc");
256+
expect(testing.normalizeParallelSessionId(" ", 1000)).toBeUndefined();
257+
expect(testing.normalizeParallelSessionId(undefined, 1000)).toBeUndefined();
258+
expect(testing.normalizeParallelSessionId("x".repeat(1001), 1000)).toBeUndefined();
259+
// Free Search MCP caps session_id at 100, so the tighter limit drops longer ids.
260+
expect(testing.normalizeParallelSessionId("x".repeat(101), 100)).toBeUndefined();
261+
expect(testing.normalizeParallelSessionId("x".repeat(100), 100)).toBe("x".repeat(100));
259262
});
260263

261264
it("normalizes client_model identifiers", () => {

0 commit comments

Comments
 (0)