From 74dd794645b583eee1db81c4e5fff97725a14185 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 3 Mar 2026 01:07:24 +0000 Subject: [PATCH] fix: stabilize generated config schema + oauthScope coverage (#43) (thanks @aryasaatvik) --- CHANGELOG.md | 1 + mcporter.schema.json | 50 +++++++++++++++------------ scripts/generate-json-schema.ts | 59 +++++++++++++++++++------------- tests/config-schema-file.test.ts | 29 ++++++++++++++++ 4 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 tests/config-schema-file.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8feea52..f0e28c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Added `CallResult.images()` plus opt-in `mcporter call --save-images ` so image content blocks can be persisted without changing existing stdout output contracts. (PR #61, thanks @daniella-11ways) - OAuth transport retries now classify HTTP 405 as HTTP (not auth) and OAuth promotion applies to configured HTTP servers too, so post-auth fallback flows no longer drop credentials on 405-only endpoints. (PR #48, thanks @caseyg) - Config loading now parses project and explicit config files as JSONC, so `mcporter.json` / `mcporter.jsonc` can include comments and trailing commas. (PR #42, thanks @aryasaatvik) +- Added generated `mcporter.schema.json` plus `pnpm generate:schema` for IDE autocomplete/validation, including `$schema` and `oauthScope`/`oauth_scope` coverage. (PR #43, thanks @aryasaatvik) ### Tooling / Dependencies - Updated dependencies to latest releases (including MCP SDK, Rolldown RC, Zod, Biome, Oxlint, Vitest, Bun types). diff --git a/mcporter.schema.json b/mcporter.schema.json index de40155..2b02d78 100644 --- a/mcporter.schema.json +++ b/mcporter.schema.json @@ -1,17 +1,13 @@ { - "$id": "https://raw.githubusercontent.com/steipete/mcporter/main/mcporter.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "mcporter configuration file schema", "type": "object", "properties": { "mcpServers": { - "description": "Map of server names to their configurations", "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { - "description": "MCP server definition supporting both HTTP/SSE and stdio transports", "type": "object", "properties": { "description": { @@ -111,16 +107,24 @@ "description": "Custom OAuth redirect URL (snake_case)", "type": "string" }, + "oauthScope": { + "description": "OAuth scope override (camelCase)", + "type": "string" + }, + "oauth_scope": { + "description": "OAuth scope override (snake_case)", + "type": "string" + }, "oauthCommand": { "description": "Custom OAuth command configuration for stdio servers (camelCase)", "type": "object", "properties": { "args": { - "description": "Arguments for the OAuth command", "type": "array", "items": { "type": "string" - } + }, + "description": "Arguments for the OAuth command" } }, "required": [ @@ -133,11 +137,11 @@ "type": "object", "properties": { "args": { - "description": "Arguments for the OAuth command", "type": "array", "items": { "type": "string" - } + }, + "description": "Arguments for the OAuth command" } }, "required": [ @@ -162,23 +166,21 @@ "type": "string" }, "lifecycle": { - "description": "Server connection lifecycle: keep-alive maintains persistent connections, ephemeral connects on-demand", "anyOf": [ { - "description": "Keep the server connection alive", "type": "string", - "const": "keep-alive" + "const": "keep-alive", + "description": "Keep the server connection alive" }, { - "description": "Connect only when needed", "type": "string", - "const": "ephemeral" + "const": "ephemeral", + "description": "Connect only when needed" }, { "type": "object", "properties": { "mode": { - "description": "Connection lifecycle mode", "anyOf": [ { "type": "string", @@ -188,7 +190,8 @@ "type": "string", "const": "ephemeral" } - ] + ], + "description": "Connection lifecycle mode" }, "idleTimeoutMs": { "description": "Idle timeout in milliseconds before disconnecting", @@ -202,7 +205,8 @@ ], "additionalProperties": false } - ] + ], + "description": "Server connection lifecycle: keep-alive maintains persistent connections, ephemeral connects on-demand" }, "logging": { "description": "Logging configuration for the server", @@ -223,14 +227,15 @@ "additionalProperties": false } }, - "additionalProperties": false - } + "additionalProperties": false, + "description": "MCP server definition supporting both HTTP/SSE and stdio transports" + }, + "description": "Map of server names to their configurations" }, "imports": { "description": "Editor configurations to import servers from. Omit to use defaults, or set to [] to disable imports", "type": "array", "items": { - "description": "Supported editor/client configurations to import MCP servers from", "type": "string", "enum": [ "cursor", @@ -240,7 +245,8 @@ "windsurf", "opencode", "vscode" - ] + ], + "description": "Supported editor/client configurations to import MCP servers from" } }, "$schema": { @@ -251,5 +257,7 @@ "required": [ "mcpServers" ], - "additionalProperties": false + "additionalProperties": false, + "description": "mcporter configuration file schema", + "$id": "https://raw.githubusercontent.com/steipete/mcporter/main/mcporter.schema.json" } diff --git a/scripts/generate-json-schema.ts b/scripts/generate-json-schema.ts index f0bf319..b4b6703 100644 --- a/scripts/generate-json-schema.ts +++ b/scripts/generate-json-schema.ts @@ -1,37 +1,50 @@ #!/usr/bin/env tsx + /** * Generate JSON Schema from Zod schemas using Zod v4's native toJSONSchema() method. * Run with: pnpm generate:schema */ import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { RawConfigSchema } from '../src/config-schema.js'; -const jsonSchema = RawConfigSchema.toJSONSchema({ - override(ctx) { - const schema = ctx.jsonSchema; - // Disallow additional properties on objects for stricter validation - if (schema?.type === 'object' && schema.additionalProperties === undefined) { - schema.additionalProperties = false; - } - }, -}); +export const CONFIG_SCHEMA_ID = 'https://raw.githubusercontent.com/steipete/mcporter/main/mcporter.schema.json'; +export const CONFIG_SCHEMA_DRAFT = 'https://json-schema.org/draft/2020-12/schema'; -// Allow $schema property in config files for IDE support -if (jsonSchema.properties && typeof jsonSchema.properties === 'object') { - (jsonSchema.properties as Record).$schema = { - type: 'string', - description: 'JSON Schema URL for IDE validation and autocomplete', +export function buildConfigJsonSchema(): Record { + const jsonSchema = RawConfigSchema.toJSONSchema({ + override(ctx) { + const schema = ctx.jsonSchema; + // Disallow additional properties on objects for stricter validation + if (schema?.type === 'object' && schema.additionalProperties === undefined) { + schema.additionalProperties = false; + } + }, + }); + + // Allow $schema property in config files for IDE support + if (jsonSchema.properties && typeof jsonSchema.properties === 'object') { + (jsonSchema.properties as Record).$schema = { + type: 'string', + description: 'JSON Schema URL for IDE validation and autocomplete', + }; + } + + return { + ...jsonSchema, + $id: CONFIG_SCHEMA_ID, + $schema: CONFIG_SCHEMA_DRAFT, }; } -// Add standard JSON Schema metadata with $id first for cleaner ordering -const orderedSchema = { - $id: 'https://raw.githubusercontent.com/steipete/mcporter/main/mcporter.schema.json', - $schema: 'http://json-schema.org/draft-07/schema#', - ...jsonSchema, -}; +export function writeConfigJsonSchema(outputPath = 'mcporter.schema.json'): void { + const schema = buildConfigJsonSchema(); + writeFileSync(outputPath, `${JSON.stringify(schema, null, 2)}\n`); + console.log(`Generated: ${outputPath}`); +} -const outputPath = 'mcporter.schema.json'; -writeFileSync(outputPath, `${JSON.stringify(orderedSchema, null, 2)}\n`); -console.log(`Generated: ${outputPath}`); +const entryPath = process.argv[1]; +if (entryPath && fileURLToPath(import.meta.url) === entryPath) { + writeConfigJsonSchema(); +} diff --git a/tests/config-schema-file.test.ts b/tests/config-schema-file.test.ts new file mode 100644 index 0000000..d3a6f9b --- /dev/null +++ b/tests/config-schema-file.test.ts @@ -0,0 +1,29 @@ +import fs from 'node:fs/promises'; +import { describe, expect, it } from 'vitest'; +import { buildConfigJsonSchema, CONFIG_SCHEMA_DRAFT } from '../scripts/generate-json-schema.js'; + +describe('generated config schema', () => { + it('stays in sync with the checked-in schema file', async () => { + const schemaPath = new URL('../mcporter.schema.json', import.meta.url); + const checkedIn = JSON.parse(await fs.readFile(schemaPath, 'utf8')) as Record; + const generated = buildConfigJsonSchema(); + expect(checkedIn).toEqual(generated); + }); + + it('includes top-level $schema and oauthScope properties', async () => { + const schemaPath = new URL('../mcporter.schema.json', import.meta.url); + const schema = JSON.parse(await fs.readFile(schemaPath, 'utf8')) as { + $schema?: string; + properties?: Record; + }; + expect(schema.$schema).toBe(CONFIG_SCHEMA_DRAFT); + expect(schema.properties?.$schema).toBeDefined(); + + const mcpServers = schema.properties?.mcpServers as + | { additionalProperties?: { properties?: Record } } + | undefined; + const entryProperties = mcpServers?.additionalProperties?.properties; + expect(entryProperties?.oauthScope).toBeDefined(); + expect(entryProperties?.oauth_scope).toBeDefined(); + }); +});