Skip to content

Commit 663b3d4

Browse files
authored
Merge pull request #713 from code-yeongyu/fix/mcp-null-tool-schema
fix(mcp): strip null-valued schema types
2 parents 45ba746 + 32dd776 commit 663b3d4

4 files changed

Lines changed: 54 additions & 0 deletions

File tree

packages/coding-agent/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
- Fixed dynamic project rules being re-injected for every distinct matching tool target: unchanged rule content now stays deduplicated while it remains in the live agent context, then becomes eligible again after accepted compaction or a rule-content change ([#712](https://github.com/code-yeongyu/senpi/pull/712)).
1616
- Fixed MCP prompt slash commands going missing after startup-raced server connections: the MCP service now emits a catalog-registration signal after publishing each snapshot, and prompt command registration waits for the server's prompt metadata instead of inferring readiness from tool registration ([#706](https://github.com/code-yeongyu/senpi/pull/706)).
17+
- Fixed HTTP 400 failures from MCP servers that emit invalid JSON-null `type` keywords by stripping only those malformed values at the shared MCP schema-conversion boundary while preserving valid JSON Schema null types ([#713](https://github.com/code-yeongyu/senpi/pull/713)).
1718

1819
### Removed
1920

packages/coding-agent/src/core/extensions/builtin/mcp/changes.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# mcp Extension Changes
22

3+
## Strip invalid null-valued MCP schema types (2026-08-04)
4+
5+
### What changed
6+
- `expose/schema-compat.ts` now omits JSON-null `type` keywords while
7+
recursively resolving MCP tool input schemas into TypeBox definitions.
8+
- Valid JSON Schema null types remain unchanged, including `type: "null"` and
9+
union arrays such as `type: ["string", "null"]`.
10+
- `test/mcp/schema-compat.test.ts` covers root, nested property, and combiner
11+
branch null values plus both valid null-type forms.
12+
13+
### Why
14+
- Some MCP servers emit `type: null`. JSON Schema permits the string `"null"`
15+
but not the JSON null value; strict OpenAI-compatible providers reject the
16+
malformed tool definition with HTTP 400 before the model can answer.
17+
- Sanitizing at MCP conversion protects every provider adapter that receives
18+
the registered tool, rather than patching one provider-specific wire path.
19+
20+
### Why extension system couldn't handle this alone
21+
- The MCP builtin owns conversion from external `tools/list` schemas to the
22+
registered `ToolDefinition`. Other extensions cannot rewrite that private
23+
schema conversion before the tool enters the shared provider pipeline.
24+
25+
### Expected merge conflict zones
26+
- LOW: `expose/schema-compat.ts` recursive `$ref` copy loop.
27+
- LOW: `test/mcp/schema-compat.test.ts` schema-conversion cases.
28+
329
## Session-expiry retry uses the full service reconnect (2026-08-03)
430

531
### What changed

packages/coding-agent/src/core/extensions/builtin/mcp/expose/schema-compat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ function resolveRefs(value: unknown, root: unknown, seenRefs: Set<string>, warni
152152

153153
const result: Record<string, unknown> = {};
154154
for (const [key, child] of Object.entries(value)) {
155+
if (key === "type" && child === null) continue;
155156
result[key] = resolveRefs(child, root, seenRefs, warnings);
156157
}
157158
return result;

packages/coding-agent/test/mcp/schema-compat.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@ describe("mcp schema compatibility", () => {
2727
expect(Object.getOwnPropertyDescriptor(result.schema, "additionalProperties")).toBeUndefined();
2828
});
2929

30+
it("strips null-valued type keywords recursively", () => {
31+
const result = convertJsonSchemaToTypeBox({
32+
type: null,
33+
properties: {
34+
pattern: { type: null, description: "ast-grep pattern" },
35+
jsonNull: { type: "null" },
36+
maybeText: { type: ["string", "null"] },
37+
choice: {
38+
anyOf: [{ type: null, const: "all" }, { type: "string" }],
39+
},
40+
},
41+
});
42+
43+
expect(result.warnings).toEqual([]);
44+
expect(JSON.parse(JSON.stringify(result.schema))).toEqual({
45+
properties: {
46+
pattern: { description: "ast-grep pattern" },
47+
jsonNull: { type: "null" },
48+
maybeText: { type: ["string", "null"] },
49+
choice: {
50+
anyOf: [{ const: "all" }, { type: "string" }],
51+
},
52+
},
53+
});
54+
});
55+
3056
it("falls back to a permissive object and warning for unresolvable refs", () => {
3157
const result = convertJsonSchemaToTypeBox({
3258
type: "object",

0 commit comments

Comments
 (0)