Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to `@pascal-app/mcp` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- Tool schemas in `tools/list` now declare the JSON Schema 2020-12 dialect
instead of the SDK default `draft-07`, so clients that enforce 2020-12 no
longer reject every tool call.

## [0.1.0] - 2026-04-18

### Added
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { registerPrompts } from './prompts'
import { registerResources } from './resources'
import type { SceneStore } from './storage/types'
import { registerTools } from './tools'
import { normalizeToolSchemaDialect } from './tools/normalize-schema-dialect'
import { registerVisionTools } from './tools/vision'
import { version } from './version'

Expand All @@ -28,5 +29,6 @@ export function createPascalMcpServer(opts: CreatePascalMcpServerOptions): McpSe
registerVisionTools(server, operations)
registerResources(server, operations)
registerPrompts(server, operations)
normalizeToolSchemaDialect(server)
return server
}
35 changes: 35 additions & 0 deletions packages/mcp/src/tools/normalize-schema-dialect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'

const DIALECT_2020_12 = 'https://json-schema.org/draft/2020-12/schema'

type RequestHandler = (request: unknown, extra: unknown) => Promise<unknown>

type HandlerRegistry = {
_requestHandlers: Map<string, RequestHandler>
}

function retargetDialect(value: unknown): void {
if (Array.isArray(value)) {
for (const item of value) retargetDialect(item)
return
}
if (value && typeof value === 'object') {
const record = value as Record<string, unknown>
if (typeof record.$schema === 'string') record.$schema = DIALECT_2020_12
for (const key of Object.keys(record)) retargetDialect(record[key])
}
}

export function normalizeToolSchemaDialect(server: McpServer): void {
const registry = server.server as unknown as HandlerRegistry
const original = registry._requestHandlers.get('tools/list')
if (!original) return

server.server.removeRequestHandler('tools/list')
server.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
const result = (await original(request, extra)) as { tools?: unknown }
retargetDialect(result.tools)
return result
})
}
37 changes: 37 additions & 0 deletions packages/mcp/src/tools/schema-dialect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, test } from 'bun:test'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { SceneBridge } from '../bridge/scene-bridge'
import { createPascalMcpServer } from '../server'

const DIALECT_2020_12 = 'https://json-schema.org/draft/2020-12/schema'

async function listRawSchemas() {
const bridge = new SceneBridge()
bridge.setScene({}, [])
bridge.loadDefault()

const server = createPascalMcpServer({ bridge })
const [srvT, cliT] = InMemoryTransport.createLinkedPair()
const client = new Client({ name: 'schema-dialect', version: '0.0.0' })
await Promise.all([server.connect(srvT), client.connect(cliT)])
const { tools } = await client.listTools()

const dialects = new Set<string>()
for (const tool of tools) {
const input = tool.inputSchema as Record<string, unknown> | undefined
const output = tool.outputSchema as Record<string, unknown> | undefined
if (typeof input?.$schema === 'string') dialects.add(input.$schema)
if (typeof output?.$schema === 'string') dialects.add(output.$schema)
}
return dialects
}

describe('tools/list schema dialect', () => {
test('every declared $schema is JSON Schema 2020-12', async () => {
const dialects = await listRawSchemas()

expect(dialects.size).toBeGreaterThan(0)
expect([...dialects]).toEqual([DIALECT_2020_12])
})
})