fix: stabilize generated config schema + oauthScope coverage (#43) (thanks @aryasaatvik)

This commit is contained in:
Peter Steinberger 2026-03-03 01:07:24 +00:00
parent 1dc2e70061
commit 74dd794645
4 changed files with 95 additions and 44 deletions

View File

@ -12,6 +12,7 @@
- Added `CallResult.images()` plus opt-in `mcporter call --save-images <dir>` 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).

View File

@ -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"
}

View File

@ -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<string, unknown>).$schema = {
type: 'string',
description: 'JSON Schema URL for IDE validation and autocomplete',
export function buildConfigJsonSchema(): Record<string, unknown> {
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<string, unknown>).$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();
}

View File

@ -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<string, unknown>;
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<string, unknown>;
};
expect(schema.$schema).toBe(CONFIG_SCHEMA_DRAFT);
expect(schema.properties?.$schema).toBeDefined();
const mcpServers = schema.properties?.mcpServers as
| { additionalProperties?: { properties?: Record<string, unknown> } }
| undefined;
const entryProperties = mcpServers?.additionalProperties?.properties;
expect(entryProperties?.oauthScope).toBeDefined();
expect(entryProperties?.oauth_scope).toBeDefined();
});
});