diff --git a/tests/features.test.ts b/tests/features.test.ts index dabcca0..2f39ca8 100644 --- a/tests/features.test.ts +++ b/tests/features.test.ts @@ -3,11 +3,13 @@ // protobuf framing, and the IR collision/de-duplication fixes surfaced by the real-spec gallery. import assert from "node:assert/strict"; import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildIR } from "../src/ir.js"; import { generateTargets } from "../src/generators/index.js"; import { renderConnectFiles } from "../src/connectgen.js"; import { paginationShape } from "../src/generators/common.js"; +import { renderMcpFiles } from "../src/mcpgen.js"; const SPEC = { openapi: "3.0.0", @@ -114,6 +116,86 @@ function buildFeatureIR() { }); } +// Source: https://github.com/Xquik-dev/x-twitter-scraper +// Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp. +function buildXquikMcpIR() { + const spec = { + openapi: "3.1.0", + info: { title: "Xquik API", version: "1.0" }, + servers: [{ url: "https://xquik.com/api/v1" }], + components: { + securitySchemes: { + ApiKeyAuth: { type: "apiKey", in: "header", name: "x-api-key" }, + }, + schemas: { + PaginatedTweets: { + type: "object", + required: ["tweets", "has_next_page", "next_cursor"], + properties: { + tweets: { type: "array", items: { type: "object" } }, + has_next_page: { type: "boolean" }, + next_cursor: { type: "string" }, + }, + }, + }, + }, + paths: { + "/x/tweets/search": { + get: { + operationId: "searchTweets", + tags: ["x"], + summary: "Search public tweets", + parameters: [ + { name: "q", in: "query", required: true, schema: { type: "string" } }, + { name: "limit", in: "query", schema: { type: "integer" } }, + ], + responses: { + "200": { + description: "Search results", + content: { "application/json": { schema: { $ref: "#/components/schemas/PaginatedTweets" } } }, + }, + }, + security: [{ ApiKeyAuth: [] }], + }, + }, + }, + }; + const config = { + sdkgen: 1, + project: { name: "xquik", display_name: "Xquik" }, + spec: { path: "./openapi.yaml" }, + targets: { + typescript: { package_name: "@example/xquik" }, + }, + client: { + class_name: "Xquik", + env_prefix: "XQUIK", + base_url: { default: "https://xquik.com/api/v1" }, + auth: { + api_key: { + env: "XQUIK_API_KEY", + security_scheme: "ApiKeyAuth", + header: "x-api-key", + }, + }, + }, + mcp: { + tools: "typed", + permissions: { + allow_http_gets: true, + }, + }, + }; + + return buildIR({ + config: config as never, + configRaw: JSON.stringify(config), + spec: spec as never, + specRaw: JSON.stringify(spec), + diagnostics: [], + }); +} + async function read(dir: string, rel: string): Promise { return readFile(join(dir, rel), "utf8"); } @@ -156,8 +238,24 @@ async function main(): Promise { assert.equal(ir.client.oauth2?.device_authorization_url, "/oauth/device", "oauth device_authorization_url"); assert.ok(ir.client.auth?.basic, "basic auth configured"); + // MCP auth: API-key header APIs should not fall back to bearer auth. + const xquikMcpServer = renderMcpFiles(buildXquikMcpIR())["src/server.ts"] ?? ""; + assert.match(xquikMcpServer, /"bearerEnv": null/, "MCP disables bearer fallback"); + assert.match(xquikMcpServer, /"name": "x-api-key"/, "MCP keeps API-key header name"); + assert.match(xquikMcpServer, /"env": "XQUIK_API_KEY"/, "MCP keeps API-key env"); + assert.match( + xquikMcpServer, + /const baseUrl = process\.env\.XQUIK_BASE_URL \?\? "https:\/\/xquik\.com\/api\/v1"/, + "MCP keeps the Xquik API base URL", + ); + assert.match( + xquikMcpServer, + /headers\[authConfig\.apiKeyHeader\.name\.toLowerCase\(\)\] = key/, + "MCP applies API-key header", + ); + // --- Generated output across languages --- - const dir = await mkdtemp("/tmp/feat-test-"); + const dir = await mkdtemp(join(tmpdir(), "feat-test-")); try { await generateTargets(ir, ["typescript", "python", "go", "ruby", "java", "csharp"], dir);