mcpmake 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +691 -0
- package/bin/mcpmake.mjs +2 -0
- package/dist/analyzer/auth-detector.d.ts +12 -0
- package/dist/analyzer/auth-detector.js +142 -0
- package/dist/analyzer/dom-parser.d.ts +10 -0
- package/dist/analyzer/dom-parser.js +259 -0
- package/dist/analyzer/goal-crawler.d.ts +25 -0
- package/dist/analyzer/goal-crawler.js +177 -0
- package/dist/analyzer/hybrid-detector.d.ts +28 -0
- package/dist/analyzer/hybrid-detector.js +96 -0
- package/dist/analyzer/index.d.ts +12 -0
- package/dist/analyzer/index.js +8 -0
- package/dist/analyzer/screenshot-capture.d.ts +29 -0
- package/dist/analyzer/screenshot-capture.js +42 -0
- package/dist/analyzer/selector-builder.d.ts +19 -0
- package/dist/analyzer/selector-builder.js +199 -0
- package/dist/analyzer/semantic-analyzer.d.ts +13 -0
- package/dist/analyzer/semantic-analyzer.js +145 -0
- package/dist/analyzer/site-crawler.d.ts +38 -0
- package/dist/analyzer/site-crawler.js +235 -0
- package/dist/cloud/billing/billing-engine.d.ts +44 -0
- package/dist/cloud/billing/billing-engine.js +81 -0
- package/dist/cloud/billing/credit-store.d.ts +64 -0
- package/dist/cloud/billing/credit-store.js +168 -0
- package/dist/cloud/billing/index.d.ts +4 -0
- package/dist/cloud/billing/index.js +2 -0
- package/dist/cloud/billing/usage-store.d.ts +42 -0
- package/dist/cloud/billing/usage-store.js +85 -0
- package/dist/cloud/billing/usage-tracker.d.ts +38 -0
- package/dist/cloud/billing/usage-tracker.js +95 -0
- package/dist/cloud/build-pipeline.d.ts +39 -0
- package/dist/cloud/build-pipeline.js +310 -0
- package/dist/cloud/build-queue.d.ts +30 -0
- package/dist/cloud/build-queue.js +70 -0
- package/dist/cloud/caddy-manager.d.ts +18 -0
- package/dist/cloud/caddy-manager.js +97 -0
- package/dist/cloud/container-backend.d.ts +62 -0
- package/dist/cloud/container-backend.js +59 -0
- package/dist/cloud/container-manager.d.ts +64 -0
- package/dist/cloud/container-manager.js +301 -0
- package/dist/cloud/crypto.d.ts +27 -0
- package/dist/cloud/crypto.js +63 -0
- package/dist/cloud/db/index.d.ts +27 -0
- package/dist/cloud/db/index.js +53 -0
- package/dist/cloud/db/migrations.d.ts +12 -0
- package/dist/cloud/db/migrations.js +329 -0
- package/dist/cloud/db/pg-store.d.ts +45 -0
- package/dist/cloud/db/pg-store.js +336 -0
- package/dist/cloud/failure-tracker.d.ts +51 -0
- package/dist/cloud/failure-tracker.js +102 -0
- package/dist/cloud/idle-monitor.d.ts +30 -0
- package/dist/cloud/idle-monitor.js +70 -0
- package/dist/cloud/mailer.d.ts +21 -0
- package/dist/cloud/mailer.js +193 -0
- package/dist/cloud/mcp-proxy.d.ts +58 -0
- package/dist/cloud/mcp-proxy.js +203 -0
- package/dist/cloud/metric-samples.d.ts +43 -0
- package/dist/cloud/metric-samples.js +85 -0
- package/dist/cloud/metrics.d.ts +26 -0
- package/dist/cloud/metrics.js +59 -0
- package/dist/cloud/multipart.d.ts +26 -0
- package/dist/cloud/multipart.js +132 -0
- package/dist/cloud/observability.d.ts +27 -0
- package/dist/cloud/observability.js +98 -0
- package/dist/cloud/rate-limiter.d.ts +31 -0
- package/dist/cloud/rate-limiter.js +58 -0
- package/dist/cloud/request-security.d.ts +5 -0
- package/dist/cloud/request-security.js +74 -0
- package/dist/cloud/resource-monitor.d.ts +69 -0
- package/dist/cloud/resource-monitor.js +130 -0
- package/dist/cloud/secret-store.d.ts +38 -0
- package/dist/cloud/secret-store.js +103 -0
- package/dist/cloud/security.d.ts +26 -0
- package/dist/cloud/security.js +142 -0
- package/dist/cloud/server.d.ts +21 -0
- package/dist/cloud/server.js +1079 -0
- package/dist/cloud/shared-state.d.ts +72 -0
- package/dist/cloud/shared-state.js +159 -0
- package/dist/cloud/ssrf.d.ts +43 -0
- package/dist/cloud/ssrf.js +150 -0
- package/dist/cloud/store.d.ts +41 -0
- package/dist/cloud/store.js +75 -0
- package/dist/cloud/stripe.d.ts +78 -0
- package/dist/cloud/stripe.js +317 -0
- package/dist/cloud/telemetry-store.d.ts +53 -0
- package/dist/cloud/telemetry-store.js +108 -0
- package/dist/cloud/web/auth.d.ts +225 -0
- package/dist/cloud/web/auth.js +555 -0
- package/dist/cloud/web/charts.d.ts +70 -0
- package/dist/cloud/web/charts.js +178 -0
- package/dist/cloud/web/csrf.d.ts +14 -0
- package/dist/cloud/web/csrf.js +22 -0
- package/dist/cloud/web/docs.d.ts +40 -0
- package/dist/cloud/web/docs.js +174 -0
- package/dist/cloud/web/router.d.ts +25 -0
- package/dist/cloud/web/router.js +1921 -0
- package/dist/cloud/web/static/alpine.min.js +5 -0
- package/dist/cloud/web/static/favicon.svg +4 -0
- package/dist/cloud/web/static/htmx-sse.js +290 -0
- package/dist/cloud/web/static/htmx.min.js +1 -0
- package/dist/cloud/web/static/style.css +2683 -0
- package/dist/cloud/web/static-server.d.ts +13 -0
- package/dist/cloud/web/static-server.js +73 -0
- package/dist/cloud/web/template-engine.d.ts +27 -0
- package/dist/cloud/web/template-engine.js +146 -0
- package/dist/cloud/web/templates/layouts/admin.hbs +57 -0
- package/dist/cloud/web/templates/layouts/auth.hbs +138 -0
- package/dist/cloud/web/templates/layouts/base.hbs +16 -0
- package/dist/cloud/web/templates/layouts/dashboard.hbs +39 -0
- package/dist/cloud/web/templates/layouts/landing.hbs +82 -0
- package/dist/cloud/web/templates/pages/admin/overview.hbs +123 -0
- package/dist/cloud/web/templates/pages/admin/servers.hbs +129 -0
- package/dist/cloud/web/templates/pages/admin/telemetry.hbs +39 -0
- package/dist/cloud/web/templates/pages/admin/user-edit.hbs +91 -0
- package/dist/cloud/web/templates/pages/admin/users.hbs +179 -0
- package/dist/cloud/web/templates/pages/auth/forgot-password.hbs +25 -0
- package/dist/cloud/web/templates/pages/auth/login.hbs +33 -0
- package/dist/cloud/web/templates/pages/auth/register.hbs +32 -0
- package/dist/cloud/web/templates/pages/auth/reset-password.hbs +34 -0
- package/dist/cloud/web/templates/pages/dashboard/billing.hbs +140 -0
- package/dist/cloud/web/templates/pages/dashboard/create.hbs +173 -0
- package/dist/cloud/web/templates/pages/dashboard/index.hbs +8 -0
- package/dist/cloud/web/templates/pages/dashboard/server-detail.hbs +280 -0
- package/dist/cloud/web/templates/pages/dashboard/server-logs.hbs +35 -0
- package/dist/cloud/web/templates/pages/dashboard/server-metrics.hbs +63 -0
- package/dist/cloud/web/templates/pages/dashboard/servers-partial.hbs +21 -0
- package/dist/cloud/web/templates/pages/dashboard/servers.hbs +44 -0
- package/dist/cloud/web/templates/pages/docs/show.hbs +16 -0
- package/dist/cloud/web/templates/pages/errors/404.hbs +9 -0
- package/dist/cloud/web/templates/pages/errors/500.hbs +8 -0
- package/dist/cloud/web/templates/pages/landing/index.hbs +223 -0
- package/dist/cloud/web/templates/pages/legal/privacy.hbs +71 -0
- package/dist/cloud/web/templates/pages/legal/terms.hbs +73 -0
- package/dist/cloud/web/templates/partials/admin-stats.hbs +52 -0
- package/dist/cloud/web/templates/partials/flash-message.hbs +6 -0
- package/dist/cloud/web/templates/partials/pricing-table.hbs +103 -0
- package/dist/cloud/web/templates/partials/server-card.hbs +19 -0
- package/dist/cloud/web/templates/partials/status-badge.hbs +1 -0
- package/dist/commands/bundle.d.ts +18 -0
- package/dist/commands/bundle.js +82 -0
- package/dist/commands/ci.d.ts +25 -0
- package/dist/commands/ci.js +149 -0
- package/dist/commands/deploy.d.ts +24 -0
- package/dist/commands/deploy.js +145 -0
- package/dist/commands/diff.d.ts +18 -0
- package/dist/commands/diff.js +185 -0
- package/dist/commands/from/describe.d.ts +65 -0
- package/dist/commands/from/describe.js +173 -0
- package/dist/commands/from/har.d.ts +81 -0
- package/dist/commands/from/har.js +255 -0
- package/dist/commands/from/openapi.d.ts +105 -0
- package/dist/commands/from/openapi.js +302 -0
- package/dist/commands/from/postman.d.ts +51 -0
- package/dist/commands/from/postman.js +146 -0
- package/dist/commands/from/target-support.d.ts +11 -0
- package/dist/commands/from/target-support.js +33 -0
- package/dist/commands/from/url.d.ts +75 -0
- package/dist/commands/from/url.js +244 -0
- package/dist/commands/from/website.d.ts +75 -0
- package/dist/commands/from/website.js +284 -0
- package/dist/commands/lint.d.ts +24 -0
- package/dist/commands/lint.js +184 -0
- package/dist/commands/merge.d.ts +18 -0
- package/dist/commands/merge.js +161 -0
- package/dist/commands/publish.d.ts +27 -0
- package/dist/commands/publish.js +334 -0
- package/dist/commands/rescan.d.ts +40 -0
- package/dist/commands/rescan.js +255 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +87 -0
- package/dist/commands/verify.d.ts +14 -0
- package/dist/commands/verify.js +71 -0
- package/dist/config/configurable-command.d.ts +13 -0
- package/dist/config/configurable-command.js +70 -0
- package/dist/config/mcpmake-config.d.ts +68 -0
- package/dist/config/mcpmake-config.js +207 -0
- package/dist/docs/cli.md +400 -0
- package/dist/docs/mcp-2026-07-28-migration.md +78 -0
- package/dist/docs/migrate-from-stainless.md +94 -0
- package/dist/docs/quickstart.md +166 -0
- package/dist/docs/show-hn.md +26 -0
- package/dist/docs/website-servers.md +169 -0
- package/dist/emitter/code-writer.d.ts +8 -0
- package/dist/emitter/code-writer.js +25 -0
- package/dist/emitter/index.d.ts +32 -0
- package/dist/emitter/index.js +280 -0
- package/dist/emitter/mcpb-bundler.d.ts +31 -0
- package/dist/emitter/mcpb-bundler.js +172 -0
- package/dist/emitter/project-scaffolder.d.ts +4 -0
- package/dist/emitter/project-scaffolder.js +89 -0
- package/dist/emitter/python-template-loader.d.ts +4 -0
- package/dist/emitter/python-template-loader.js +30 -0
- package/dist/emitter/python-templates/dockerfile.hbs +14 -0
- package/dist/emitter/python-templates/env.example.hbs +6 -0
- package/dist/emitter/python-templates/requirements.txt.hbs +4 -0
- package/dist/emitter/python-templates/server.py.hbs +77 -0
- package/dist/emitter/site-scaffolder.d.ts +13 -0
- package/dist/emitter/site-scaffolder.js +70 -0
- package/dist/emitter/site-template-loader.d.ts +5 -0
- package/dist/emitter/site-template-loader.js +47 -0
- package/dist/emitter/site-templates/browser-manager.ts.hbs +233 -0
- package/dist/emitter/site-templates/config.ts.hbs +28 -0
- package/dist/emitter/site-templates/dockerfile.hbs +31 -0
- package/dist/emitter/site-templates/env.example.hbs +19 -0
- package/dist/emitter/site-templates/package.json.hbs +26 -0
- package/dist/emitter/site-templates/server-main-http.ts.hbs +108 -0
- package/dist/emitter/site-templates/server-main.ts.hbs +23 -0
- package/dist/emitter/site-templates/tool-handler-action.ts.hbs +86 -0
- package/dist/emitter/site-templates/tool-handler-form.ts.hbs +116 -0
- package/dist/emitter/site-templates/tool-handler-lifecycle.ts.hbs +146 -0
- package/dist/emitter/site-templates/tool-index.ts.hbs +11 -0
- package/dist/emitter/template-loader.d.ts +1 -0
- package/dist/emitter/template-loader.js +27 -0
- package/dist/emitter/templates/auth-provider.ts.hbs +57 -0
- package/dist/emitter/templates/config.ts.hbs +63 -0
- package/dist/emitter/templates/discovery.ts.hbs +301 -0
- package/dist/emitter/templates/dockerfile.hbs +34 -0
- package/dist/emitter/templates/env.example.hbs +28 -0
- package/dist/emitter/templates/gitignore.hbs +5 -0
- package/dist/emitter/templates/http-executor.ts.hbs +117 -0
- package/dist/emitter/templates/oauth.ts.hbs +188 -0
- package/dist/emitter/templates/package.json.hbs +25 -0
- package/dist/emitter/templates/prompts.ts.hbs +22 -0
- package/dist/emitter/templates/readme.md.hbs +123 -0
- package/dist/emitter/templates/resources.ts.hbs +63 -0
- package/dist/emitter/templates/server-main-http.ts.hbs +407 -0
- package/dist/emitter/templates/server-main.ts.hbs +40 -0
- package/dist/emitter/templates/task-handlers.ts.hbs +189 -0
- package/dist/emitter/templates/task-manager.ts.hbs +139 -0
- package/dist/emitter/templates/task-sse.ts.hbs +105 -0
- package/dist/emitter/templates/tool-handler.ts.hbs +124 -0
- package/dist/emitter/templates/tool-index.ts.hbs +11 -0
- package/dist/emitter/templates/tool-test.ts.hbs +57 -0
- package/dist/emitter/templates/trace.ts.hbs +79 -0
- package/dist/emitter/templates/tsconfig.json.hbs +16 -0
- package/dist/emitter/templates/types.ts.hbs +5 -0
- package/dist/emitter/worker-template-loader.d.ts +5 -0
- package/dist/emitter/worker-template-loader.js +33 -0
- package/dist/emitter/worker-templates/config.ts.hbs +54 -0
- package/dist/emitter/worker-templates/dev-vars.example.hbs +10 -0
- package/dist/emitter/worker-templates/gitignore.hbs +6 -0
- package/dist/emitter/worker-templates/package.json.hbs +24 -0
- package/dist/emitter/worker-templates/readme.md.hbs +53 -0
- package/dist/emitter/worker-templates/server.test.ts.hbs +20 -0
- package/dist/emitter/worker-templates/tool-handler.ts.hbs +85 -0
- package/dist/emitter/worker-templates/tool-index.ts.hbs +28 -0
- package/dist/emitter/worker-templates/tsconfig.json.hbs +17 -0
- package/dist/emitter/worker-templates/worker.ts.hbs +242 -0
- package/dist/emitter/worker-templates/wrangler.toml.hbs +19 -0
- package/dist/generator/spec-generator.d.ts +6 -0
- package/dist/generator/spec-generator.js +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +64 -0
- package/dist/parser/har-filter.d.ts +8 -0
- package/dist/parser/har-filter.js +71 -0
- package/dist/parser/har-loader.d.ts +2 -0
- package/dist/parser/har-loader.js +14 -0
- package/dist/parser/har-normalizer.d.ts +20 -0
- package/dist/parser/har-normalizer.js +78 -0
- package/dist/parser/index.d.ts +10 -0
- package/dist/parser/index.js +6 -0
- package/dist/parser/openapi-loader.d.ts +6 -0
- package/dist/parser/openapi-loader.js +308 -0
- package/dist/parser/operation-extractor.d.ts +13 -0
- package/dist/parser/operation-extractor.js +155 -0
- package/dist/parser/overlay-loader.d.ts +10 -0
- package/dist/parser/overlay-loader.js +184 -0
- package/dist/parser/postman-loader.d.ts +9 -0
- package/dist/parser/postman-loader.js +106 -0
- package/dist/parser/schema-converter.d.ts +12 -0
- package/dist/parser/schema-converter.js +117 -0
- package/dist/plugins/adapter.d.ts +40 -0
- package/dist/plugins/adapter.js +15 -0
- package/dist/plugins/loader.d.ts +25 -0
- package/dist/plugins/loader.js +58 -0
- package/dist/pricing.d.ts +55 -0
- package/dist/pricing.js +133 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.js +56 -0
- package/dist/recorder/browser-recorder.d.ts +22 -0
- package/dist/recorder/browser-recorder.js +205 -0
- package/dist/registry/official-registry.d.ts +90 -0
- package/dist/registry/official-registry.js +129 -0
- package/dist/rescan/diff-engine.d.ts +5 -0
- package/dist/rescan/diff-engine.js +312 -0
- package/dist/rescan/index.d.ts +3 -0
- package/dist/rescan/index.js +2 -0
- package/dist/rescan/rescan-runner.d.ts +42 -0
- package/dist/rescan/rescan-runner.js +69 -0
- package/dist/rescan/rescan-scheduler.d.ts +41 -0
- package/dist/rescan/rescan-scheduler.js +179 -0
- package/dist/site-transformer/browser-tools.d.ts +10 -0
- package/dist/site-transformer/browser-tools.js +59 -0
- package/dist/site-transformer/index.d.ts +2 -0
- package/dist/site-transformer/index.js +2 -0
- package/dist/site-transformer/selector-healer.d.ts +8 -0
- package/dist/site-transformer/selector-healer.js +106 -0
- package/dist/site-transformer/tool-generator.d.ts +13 -0
- package/dist/site-transformer/tool-generator.js +245 -0
- package/dist/transformer/auth-detector.d.ts +13 -0
- package/dist/transformer/auth-detector.js +90 -0
- package/dist/transformer/catalog-builder.d.ts +18 -0
- package/dist/transformer/catalog-builder.js +56 -0
- package/dist/transformer/client-compat.d.ts +6 -0
- package/dist/transformer/client-compat.js +44 -0
- package/dist/transformer/har-clusterer.d.ts +9 -0
- package/dist/transformer/har-clusterer.js +27 -0
- package/dist/transformer/har-dedup.d.ts +10 -0
- package/dist/transformer/har-dedup.js +81 -0
- package/dist/transformer/har-schema-inferrer.d.ts +15 -0
- package/dist/transformer/har-schema-inferrer.js +90 -0
- package/dist/transformer/har-to-operations.d.ts +13 -0
- package/dist/transformer/har-to-operations.js +192 -0
- package/dist/transformer/index.d.ts +8 -0
- package/dist/transformer/index.js +6 -0
- package/dist/transformer/llm-namer.d.ts +6 -0
- package/dist/transformer/llm-namer.js +59 -0
- package/dist/transformer/naming.d.ts +4 -0
- package/dist/transformer/naming.js +30 -0
- package/dist/transformer/operation-filter.d.ts +13 -0
- package/dist/transformer/operation-filter.js +52 -0
- package/dist/transformer/resource-builder.d.ts +12 -0
- package/dist/transformer/resource-builder.js +80 -0
- package/dist/transformer/schema-merger.d.ts +14 -0
- package/dist/transformer/schema-merger.js +65 -0
- package/dist/transformer/tool-builder.d.ts +3 -0
- package/dist/transformer/tool-builder.js +114 -0
- package/dist/types/index.d.ts +131 -0
- package/dist/types/index.js +1 -0
- package/dist/types/site.d.ts +284 -0
- package/dist/types/site.js +8 -0
- package/dist/utils/fail.d.ts +48 -0
- package/dist/utils/fail.js +204 -0
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +28 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +30 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.js +2 -0
- package/dist/utils/sanitize.d.ts +28 -0
- package/dist/utils/sanitize.js +44 -0
- package/dist/utils/watcher.d.ts +11 -0
- package/dist/utils/watcher.js +36 -0
- package/package.json +65 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { defineConfigurableCommand } from '../../config/configurable-command.js';
|
|
2
|
+
import { crawlSite } from '../../analyzer/site-crawler.js';
|
|
3
|
+
import { goalDirectedCrawl } from '../../analyzer/goal-crawler.js';
|
|
4
|
+
import { classifyForms } from '../../analyzer/hybrid-detector.js';
|
|
5
|
+
import { analyzeSemantics } from '../../analyzer/semantic-analyzer.js';
|
|
6
|
+
import { detectAuthFlow } from '../../analyzer/auth-detector.js';
|
|
7
|
+
import { generateSiteTools } from '../../site-transformer/tool-generator.js';
|
|
8
|
+
import { filterHarEntries } from '../../parser/har-filter.js';
|
|
9
|
+
import { normalizeEntry } from '../../parser/har-normalizer.js';
|
|
10
|
+
import { clusterEntries } from '../../transformer/har-clusterer.js';
|
|
11
|
+
import { clustersToOperations } from '../../transformer/har-to-operations.js';
|
|
12
|
+
import { deduplicateEntries } from '../../transformer/har-dedup.js';
|
|
13
|
+
import { buildAllTools } from '../../transformer/tool-builder.js';
|
|
14
|
+
import { emitSiteProject } from '../../emitter/index.js';
|
|
15
|
+
import { logger } from '../../utils/logger.js';
|
|
16
|
+
import { fail } from '../../utils/fail.js';
|
|
17
|
+
function toPackageName(name) {
|
|
18
|
+
return name
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
+
.replace(/^-|-$/g, '');
|
|
22
|
+
}
|
|
23
|
+
export default defineConfigurableCommand('website', {
|
|
24
|
+
meta: {
|
|
25
|
+
name: 'website',
|
|
26
|
+
description: "Generate a Playwright-based MCP server by analyzing a website's DOM",
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
url: {
|
|
30
|
+
type: 'positional',
|
|
31
|
+
description: 'URL of the website to analyze',
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
output: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
alias: 'o',
|
|
37
|
+
description: 'Output directory for generated project',
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
name: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
alias: 'n',
|
|
43
|
+
description: 'Server name (defaults to hostname)',
|
|
44
|
+
},
|
|
45
|
+
depth: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Crawl depth (default: 2)',
|
|
48
|
+
default: '2',
|
|
49
|
+
},
|
|
50
|
+
'max-pages': {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Maximum pages to crawl (default: 20)',
|
|
53
|
+
default: '20',
|
|
54
|
+
},
|
|
55
|
+
timeout: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Idle timeout in seconds (default: 300)',
|
|
58
|
+
default: '300',
|
|
59
|
+
},
|
|
60
|
+
transport: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
alias: 't',
|
|
63
|
+
description: 'Transport mode: "stdio" (default) or "http"',
|
|
64
|
+
default: 'stdio',
|
|
65
|
+
},
|
|
66
|
+
headless: {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
description: 'Run browser in headless mode during analysis',
|
|
69
|
+
default: false,
|
|
70
|
+
},
|
|
71
|
+
force: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
alias: 'f',
|
|
74
|
+
description: 'Overwrite existing output directory',
|
|
75
|
+
default: false,
|
|
76
|
+
},
|
|
77
|
+
'dry-run': {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Preview generated files without writing',
|
|
80
|
+
default: false,
|
|
81
|
+
},
|
|
82
|
+
'improve-names': {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'Use LLM to infer semantic names for forms, buttons, and links',
|
|
85
|
+
default: false,
|
|
86
|
+
},
|
|
87
|
+
'max-sessions': {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Maximum concurrent browser sessions for the generated server (default: 10)',
|
|
90
|
+
default: '10',
|
|
91
|
+
},
|
|
92
|
+
hybrid: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description: 'Hybrid mode: use HTTP fetch for API-backed forms and Playwright for browser-only forms',
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
goal: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Goal-directed crawl: use an LLM to navigate toward a goal instead of BFS crawling (requires ANTHROPIC_API_KEY)',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
async run({ args }) {
|
|
103
|
+
const url = args.url;
|
|
104
|
+
const depth = parseInt(args.depth ?? '2', 10);
|
|
105
|
+
const maxPages = parseInt(args['max-pages'] ?? '20', 10);
|
|
106
|
+
const timeoutMs = parseInt(args.timeout ?? '300', 10) * 1000;
|
|
107
|
+
const maxSessions = parseInt(args['max-sessions'] ?? '10', 10);
|
|
108
|
+
const hybridMode = args.hybrid ?? false;
|
|
109
|
+
const goal = args.goal;
|
|
110
|
+
// Validate URL
|
|
111
|
+
let parsedUrl;
|
|
112
|
+
try {
|
|
113
|
+
parsedUrl = new URL(url);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return await fail(`Invalid URL: ${url}`, err);
|
|
117
|
+
}
|
|
118
|
+
logger.info(`Analyzing website: ${url}`);
|
|
119
|
+
let siteDescriptor;
|
|
120
|
+
let screenshots;
|
|
121
|
+
let harEntries;
|
|
122
|
+
if (goal) {
|
|
123
|
+
// Goal-directed crawl: use LLM to navigate toward the goal
|
|
124
|
+
logger.info(`Goal-directed crawl: "${goal}"`);
|
|
125
|
+
const result = await goalDirectedCrawl({
|
|
126
|
+
url,
|
|
127
|
+
goal,
|
|
128
|
+
maxSteps: maxPages,
|
|
129
|
+
headless: args.headless ?? false,
|
|
130
|
+
});
|
|
131
|
+
siteDescriptor = result.siteDescriptor;
|
|
132
|
+
screenshots = result.screenshots;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Standard BFS crawl (with optional HAR capture for hybrid mode)
|
|
136
|
+
logger.info(`Crawl depth: ${depth}, Max pages: ${maxPages}`);
|
|
137
|
+
const result = await crawlSite({
|
|
138
|
+
url,
|
|
139
|
+
depth,
|
|
140
|
+
maxPages,
|
|
141
|
+
timeout: timeoutMs,
|
|
142
|
+
headless: args.headless ?? false,
|
|
143
|
+
captureHar: hybridMode,
|
|
144
|
+
});
|
|
145
|
+
siteDescriptor = result.siteDescriptor;
|
|
146
|
+
screenshots = result.screenshots;
|
|
147
|
+
harEntries = result.harEntries;
|
|
148
|
+
}
|
|
149
|
+
const totalForms = siteDescriptor.pages.reduce((n, p) => n + p.forms.length, 0);
|
|
150
|
+
const totalButtons = siteDescriptor.pages.reduce((n, p) => n + p.buttons.length, 0);
|
|
151
|
+
const totalLinks = siteDescriptor.pages.reduce((n, p) => n + p.links.length, 0);
|
|
152
|
+
logger.info(`Discovered: ${siteDescriptor.pages.length} pages, ${totalForms} forms, ${totalButtons} buttons, ${totalLinks} links`);
|
|
153
|
+
logger.info(`Screenshots captured: ${screenshots.size}`);
|
|
154
|
+
if (siteDescriptor.pages.length === 0) {
|
|
155
|
+
await fail('No pages could be analyzed. Check the URL and try again.');
|
|
156
|
+
}
|
|
157
|
+
// Semantic analysis (optional, requires ANTHROPIC_API_KEY)
|
|
158
|
+
if (args['improve-names']) {
|
|
159
|
+
siteDescriptor.pages = await analyzeSemantics(siteDescriptor.pages, screenshots);
|
|
160
|
+
}
|
|
161
|
+
// Auth flow detection
|
|
162
|
+
const authFlow = detectAuthFlow(siteDescriptor.pages);
|
|
163
|
+
if (authFlow) {
|
|
164
|
+
siteDescriptor.authFlow = authFlow;
|
|
165
|
+
}
|
|
166
|
+
// Generate browser-based tools from site descriptor
|
|
167
|
+
const siteTools = generateSiteTools(siteDescriptor);
|
|
168
|
+
// Hybrid mode: classify forms and merge API tools with browser tools
|
|
169
|
+
let tools;
|
|
170
|
+
let hybridClassifications;
|
|
171
|
+
if (hybridMode && harEntries && harEntries.length > 0) {
|
|
172
|
+
logger.info(`Hybrid mode: analyzing ${harEntries.length} captured network requests`);
|
|
173
|
+
// Classify all forms
|
|
174
|
+
const allForms = siteDescriptor.pages.flatMap((p) => p.forms);
|
|
175
|
+
hybridClassifications = classifyForms(allForms, harEntries);
|
|
176
|
+
const apiFormIds = new Set(hybridClassifications.filter((c) => c.strategy === 'api').map((c) => c.formId));
|
|
177
|
+
const apiCount = apiFormIds.size;
|
|
178
|
+
const browserCount = hybridClassifications.length - apiCount;
|
|
179
|
+
logger.info(`Hybrid classification: ${apiCount} API-backed forms, ${browserCount} browser-only forms`);
|
|
180
|
+
// Build API tools from HAR entries
|
|
181
|
+
const targetHost = new URL(url).hostname;
|
|
182
|
+
const filteredHar = filterHarEntries(harEntries, {
|
|
183
|
+
allowedDomains: [targetHost],
|
|
184
|
+
includeErrors: false,
|
|
185
|
+
});
|
|
186
|
+
if (filteredHar.length > 0) {
|
|
187
|
+
const normalized = filteredHar.map(normalizeEntry);
|
|
188
|
+
const deduped = deduplicateEntries(normalized);
|
|
189
|
+
const clusters = clusterEntries(deduped);
|
|
190
|
+
const { operations } = clustersToOperations(clusters);
|
|
191
|
+
const apiTools = buildAllTools(operations);
|
|
192
|
+
// Remove browser-based form tools that have API equivalents
|
|
193
|
+
const browserTools = siteTools.filter((tool) => {
|
|
194
|
+
if (tool.form && apiFormIds.has(tool.form.formId)) {
|
|
195
|
+
return false; // Prefer the API tool
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
});
|
|
199
|
+
// Convert API ToolDefinitions to SiteToolDefinitions for uniform output
|
|
200
|
+
const apiSiteTools = apiTools.map((apiTool) => ({
|
|
201
|
+
name: apiTool.name,
|
|
202
|
+
title: apiTool.title,
|
|
203
|
+
description: `[API] ${apiTool.description}`,
|
|
204
|
+
inputSchemaCode: apiTool.inputSchemaCode,
|
|
205
|
+
fileName: apiTool.fileName,
|
|
206
|
+
functionName: apiTool.functionName,
|
|
207
|
+
toolType: 'page-action',
|
|
208
|
+
selectors: [],
|
|
209
|
+
returnsScreenshot: false,
|
|
210
|
+
annotations: apiTool.annotations,
|
|
211
|
+
}));
|
|
212
|
+
tools = [...browserTools, ...apiSiteTools];
|
|
213
|
+
// Deduplicate by name
|
|
214
|
+
const seenNames = new Set();
|
|
215
|
+
tools = tools.filter((tool) => {
|
|
216
|
+
if (seenNames.has(tool.name))
|
|
217
|
+
return false;
|
|
218
|
+
seenNames.add(tool.name);
|
|
219
|
+
return true;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
tools = siteTools;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
tools = siteTools;
|
|
228
|
+
}
|
|
229
|
+
logger.info(`Generated ${tools.length} MCP tools`);
|
|
230
|
+
if (tools.length === 0) {
|
|
231
|
+
await fail('No interactive elements found on the site.');
|
|
232
|
+
}
|
|
233
|
+
// Build manifest
|
|
234
|
+
const serverName = args.name ?? toPackageName(parsedUrl.hostname);
|
|
235
|
+
const transport = args.transport === 'http' ? 'http' : 'stdio';
|
|
236
|
+
const browserConfig = {
|
|
237
|
+
headless: true,
|
|
238
|
+
idleTimeoutMs: 5 * 60 * 1000,
|
|
239
|
+
viewport: { width: 1280, height: 720 },
|
|
240
|
+
maxSessions,
|
|
241
|
+
};
|
|
242
|
+
const manifest = {
|
|
243
|
+
serverName,
|
|
244
|
+
serverVersion: '1.0.0',
|
|
245
|
+
baseUrl: siteDescriptor.baseUrl,
|
|
246
|
+
transport: transport,
|
|
247
|
+
siteDescriptor,
|
|
248
|
+
tools,
|
|
249
|
+
envVars: [
|
|
250
|
+
{
|
|
251
|
+
name: 'BASE_URL',
|
|
252
|
+
description: 'Target website URL',
|
|
253
|
+
required: true,
|
|
254
|
+
example: siteDescriptor.baseUrl,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
browserConfig,
|
|
258
|
+
};
|
|
259
|
+
// Emit the project
|
|
260
|
+
logger.info(`Generating Playwright MCP server: ${serverName}`);
|
|
261
|
+
await emitSiteProject(manifest, {
|
|
262
|
+
outputDir: args.output,
|
|
263
|
+
force: args.force ?? false,
|
|
264
|
+
dryRun: args['dry-run'] ?? false,
|
|
265
|
+
});
|
|
266
|
+
logger.success(`Site MCP server generated at: ${args.output}`);
|
|
267
|
+
logger.info('');
|
|
268
|
+
logger.info(`Pages analyzed: ${siteDescriptor.pages.length}`);
|
|
269
|
+
logger.info(`Tools generated: ${tools.length}`);
|
|
270
|
+
logger.info('');
|
|
271
|
+
logger.info('Tools:');
|
|
272
|
+
for (const tool of tools) {
|
|
273
|
+
const typeTag = `[${tool.toolType}]`;
|
|
274
|
+
logger.info(` ${tool.name} ${typeTag} — ${tool.description.slice(0, 60)}`);
|
|
275
|
+
}
|
|
276
|
+
logger.info('');
|
|
277
|
+
logger.info('Next steps:');
|
|
278
|
+
logger.info(` cd ${args.output}`);
|
|
279
|
+
logger.info(' npm install');
|
|
280
|
+
logger.info(' npx playwright install chromium');
|
|
281
|
+
logger.info(' npm run build');
|
|
282
|
+
logger.info(' npm start');
|
|
283
|
+
},
|
|
284
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LintResult {
|
|
2
|
+
level: 'error' | 'warn' | 'info';
|
|
3
|
+
rule: string;
|
|
4
|
+
tool: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
declare const _default: import("citty").CommandDef<{
|
|
8
|
+
spec: {
|
|
9
|
+
type: "positional";
|
|
10
|
+
description: string;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
format: {
|
|
14
|
+
type: "string";
|
|
15
|
+
description: string;
|
|
16
|
+
default: string;
|
|
17
|
+
};
|
|
18
|
+
level: {
|
|
19
|
+
type: "string";
|
|
20
|
+
description: string;
|
|
21
|
+
default: string;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
export default _default;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { loadOpenApiSpec } from '../parser/openapi-loader.js';
|
|
3
|
+
import { extractOperations } from '../parser/operation-extractor.js';
|
|
4
|
+
import { buildAllTools } from '../transformer/tool-builder.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { fail } from '../utils/fail.js';
|
|
7
|
+
const MCP_BUILT_INS = new Set([
|
|
8
|
+
'ping',
|
|
9
|
+
'initialize',
|
|
10
|
+
'notifications/initialized',
|
|
11
|
+
'notifications/cancelled',
|
|
12
|
+
'notifications/progress',
|
|
13
|
+
'tools/list',
|
|
14
|
+
'tools/call',
|
|
15
|
+
'resources/list',
|
|
16
|
+
'resources/read',
|
|
17
|
+
'resources/subscribe',
|
|
18
|
+
'resources/unsubscribe',
|
|
19
|
+
'prompts/list',
|
|
20
|
+
'prompts/get',
|
|
21
|
+
'logging/setLevel',
|
|
22
|
+
'completion/complete',
|
|
23
|
+
'sampling/createMessage',
|
|
24
|
+
'roots/list',
|
|
25
|
+
]);
|
|
26
|
+
function lintTools(tools) {
|
|
27
|
+
const results = [];
|
|
28
|
+
// duplicate-names: check for collisions
|
|
29
|
+
const nameCount = new Map();
|
|
30
|
+
for (const tool of tools) {
|
|
31
|
+
nameCount.set(tool.name, (nameCount.get(tool.name) ?? 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
for (const [name, count] of nameCount) {
|
|
34
|
+
if (count > 1) {
|
|
35
|
+
results.push({
|
|
36
|
+
level: 'error',
|
|
37
|
+
rule: 'duplicate-names',
|
|
38
|
+
tool: name,
|
|
39
|
+
message: `Tool name "${name}" is used ${count} times — names must be unique`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const tool of tools) {
|
|
44
|
+
// tool-name-length
|
|
45
|
+
if (tool.name.length > 128) {
|
|
46
|
+
results.push({
|
|
47
|
+
level: 'error',
|
|
48
|
+
rule: 'tool-name-length',
|
|
49
|
+
tool: tool.name,
|
|
50
|
+
message: `Name is ${tool.name.length} chars — exceeds MCP spec limit of 128`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else if (tool.name.length > 60) {
|
|
54
|
+
results.push({
|
|
55
|
+
level: 'warn',
|
|
56
|
+
rule: 'tool-name-length',
|
|
57
|
+
tool: tool.name,
|
|
58
|
+
message: `Name is ${tool.name.length} chars — exceeds Cursor limit of 60`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// description-quality
|
|
62
|
+
if (!tool.description || tool.description.trim().length === 0) {
|
|
63
|
+
results.push({
|
|
64
|
+
level: 'warn',
|
|
65
|
+
rule: 'description-quality',
|
|
66
|
+
tool: tool.name,
|
|
67
|
+
message: 'Description is empty — AI clients need good descriptions to pick the right tool',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (tool.description.trim().length < 10) {
|
|
71
|
+
results.push({
|
|
72
|
+
level: 'warn',
|
|
73
|
+
rule: 'description-quality',
|
|
74
|
+
tool: tool.name,
|
|
75
|
+
message: `Description is only ${tool.description.trim().length} chars — consider adding more detail`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// description-length
|
|
79
|
+
if (tool.description && tool.description.length > 1024) {
|
|
80
|
+
results.push({
|
|
81
|
+
level: 'warn',
|
|
82
|
+
rule: 'description-length',
|
|
83
|
+
tool: tool.name,
|
|
84
|
+
message: `Description is ${tool.description.length} chars — over 1024 wastes context window`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// parameter-count
|
|
88
|
+
const paramCount = tool.pathParams.length +
|
|
89
|
+
tool.queryParams.length +
|
|
90
|
+
tool.headerParams.length +
|
|
91
|
+
(tool.hasRequestBody ? 1 : 0);
|
|
92
|
+
if (paramCount > 10) {
|
|
93
|
+
results.push({
|
|
94
|
+
level: 'warn',
|
|
95
|
+
rule: 'parameter-count',
|
|
96
|
+
tool: tool.name,
|
|
97
|
+
message: `Has ${paramCount} parameters — more than 10 is complex for AI to fill correctly`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// missing-annotations
|
|
101
|
+
if (!tool.annotations) {
|
|
102
|
+
results.push({
|
|
103
|
+
level: 'info',
|
|
104
|
+
rule: 'missing-annotations',
|
|
105
|
+
tool: tool.name,
|
|
106
|
+
message: 'No readOnlyHint/destructiveHint annotations set — consider adding for safety',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// reserved-names
|
|
110
|
+
if (MCP_BUILT_INS.has(tool.name)) {
|
|
111
|
+
results.push({
|
|
112
|
+
level: 'error',
|
|
113
|
+
rule: 'reserved-names',
|
|
114
|
+
tool: tool.name,
|
|
115
|
+
message: `"${tool.name}" conflicts with MCP built-in method name`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
export default defineCommand({
|
|
122
|
+
meta: {
|
|
123
|
+
name: 'lint',
|
|
124
|
+
description: 'Lint an OpenAPI spec for MCP compatibility issues',
|
|
125
|
+
},
|
|
126
|
+
args: {
|
|
127
|
+
spec: {
|
|
128
|
+
type: 'positional',
|
|
129
|
+
description: 'Path or URL to an OpenAPI spec',
|
|
130
|
+
required: true,
|
|
131
|
+
},
|
|
132
|
+
format: {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'Output format: "text" (default) or "json"',
|
|
135
|
+
default: 'text',
|
|
136
|
+
},
|
|
137
|
+
level: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Minimum level to show: "info", "warn", or "error"',
|
|
140
|
+
default: 'info',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
async run({ args }) {
|
|
144
|
+
logger.info(`Linting spec: ${args.spec}`);
|
|
145
|
+
const { api } = await loadOpenApiSpec(args.spec);
|
|
146
|
+
const { operations } = extractOperations(api);
|
|
147
|
+
if (operations.length === 0) {
|
|
148
|
+
await fail('No operations found in the spec.');
|
|
149
|
+
}
|
|
150
|
+
const tools = buildAllTools(operations);
|
|
151
|
+
logger.info(`Analyzing ${tools.length} tools...\n`);
|
|
152
|
+
const allResults = lintTools(tools);
|
|
153
|
+
// Filter by level
|
|
154
|
+
const levelOrder = { info: 0, warn: 1, error: 2 };
|
|
155
|
+
const minLevel = levelOrder[args.level ?? 'info'] ?? 0;
|
|
156
|
+
const results = allResults.filter((r) => levelOrder[r.level] >= minLevel);
|
|
157
|
+
if (args.format === 'json') {
|
|
158
|
+
console.log(JSON.stringify(results, null, 2));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const icons = { error: 'ERROR', warn: 'WARN ', info: 'INFO ' };
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
const icon = icons[r.level];
|
|
164
|
+
console.log(` ${icon} [${r.rule}] ${r.tool}: ${r.message}`);
|
|
165
|
+
}
|
|
166
|
+
// Summary
|
|
167
|
+
const errors = allResults.filter((r) => r.level === 'error').length;
|
|
168
|
+
const warns = allResults.filter((r) => r.level === 'warn').length;
|
|
169
|
+
const infos = allResults.filter((r) => r.level === 'info').length;
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(` ${tools.length} tools, ${allResults.length} issues: ${errors} errors, ${warns} warnings, ${infos} info`);
|
|
172
|
+
if (errors > 0) {
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
else if (allResults.length === 0) {
|
|
176
|
+
logger.success('No issues found');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Exit code 1 if any errors
|
|
180
|
+
if (allResults.some((r) => r.level === 'error')) {
|
|
181
|
+
await fail('Lint failed with errors');
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare const _default: import("citty").CommandDef<{
|
|
2
|
+
spec1: {
|
|
3
|
+
type: "positional";
|
|
4
|
+
description: string;
|
|
5
|
+
required: true;
|
|
6
|
+
};
|
|
7
|
+
spec2: {
|
|
8
|
+
type: "positional";
|
|
9
|
+
description: string;
|
|
10
|
+
required: true;
|
|
11
|
+
};
|
|
12
|
+
output: {
|
|
13
|
+
type: "string";
|
|
14
|
+
alias: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { stringify as yamlStringify } from 'yaml';
|
|
4
|
+
import { loadOpenApiSpec } from '../parser/openapi-loader.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: 'merge',
|
|
9
|
+
description: 'Merge two OpenAPI specs into one',
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
spec1: {
|
|
13
|
+
type: 'positional',
|
|
14
|
+
description: 'Path to the first OpenAPI spec (used as base for info/servers)',
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
spec2: {
|
|
18
|
+
type: 'positional',
|
|
19
|
+
description: 'Path to the second OpenAPI spec to merge in',
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
output: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
alias: 'o',
|
|
25
|
+
description: 'Output file path (default: stdout)',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async run({ args }) {
|
|
29
|
+
logger.info(`Merging specs: ${args.spec1} + ${args.spec2}`);
|
|
30
|
+
const { api: api1 } = await loadOpenApiSpec(args.spec1);
|
|
31
|
+
const { api: api2 } = await loadOpenApiSpec(args.spec2);
|
|
32
|
+
const spec1 = api1;
|
|
33
|
+
const spec2 = api2;
|
|
34
|
+
const merged = mergeSpecs(spec1, spec2);
|
|
35
|
+
const output = yamlStringify(merged, { lineWidth: 120 });
|
|
36
|
+
if (args.output) {
|
|
37
|
+
await writeFile(args.output, output, 'utf-8');
|
|
38
|
+
logger.success(`Merged spec written to: ${args.output}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stdout.write(output);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
function mergeSpecs(base, other) {
|
|
46
|
+
const merged = {
|
|
47
|
+
openapi: base.openapi ?? '3.0.0',
|
|
48
|
+
info: base.info,
|
|
49
|
+
servers: base.servers,
|
|
50
|
+
paths: { ...base.paths },
|
|
51
|
+
components: {
|
|
52
|
+
schemas: {},
|
|
53
|
+
securitySchemes: {},
|
|
54
|
+
...(base.components ?? {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
// Merge paths
|
|
58
|
+
const basePaths = base.paths ?? {};
|
|
59
|
+
const otherPaths = other.paths ?? {};
|
|
60
|
+
for (const [path, pathItem] of Object.entries(otherPaths)) {
|
|
61
|
+
if (basePaths[path]) {
|
|
62
|
+
// Check for method-level conflicts
|
|
63
|
+
const baseItem = basePaths[path];
|
|
64
|
+
const otherItem = pathItem;
|
|
65
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
66
|
+
for (const method of methods) {
|
|
67
|
+
if (baseItem[method] && otherItem[method]) {
|
|
68
|
+
throw new Error(`Path conflict: ${method.toUpperCase()} ${path} exists in both specs`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// No method conflicts — merge path items
|
|
72
|
+
merged.paths[path] = { ...baseItem, ...otherItem };
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
merged.paths[path] = pathItem;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Merge components.schemas
|
|
79
|
+
const baseSchemas = base.components?.schemas ?? {};
|
|
80
|
+
const otherSchemas = other.components?.schemas ?? {};
|
|
81
|
+
for (const [name, schema] of Object.entries(otherSchemas)) {
|
|
82
|
+
if (baseSchemas[name]) {
|
|
83
|
+
throw new Error(`Schema conflict: "${name}" exists in both specs`);
|
|
84
|
+
}
|
|
85
|
+
merged.components.schemas[name] = schema;
|
|
86
|
+
}
|
|
87
|
+
// Merge components.securitySchemes
|
|
88
|
+
const baseSecSchemes = base.components?.securitySchemes ?? {};
|
|
89
|
+
const otherSecSchemes = other.components?.securitySchemes ?? {};
|
|
90
|
+
for (const [name, scheme] of Object.entries(otherSecSchemes)) {
|
|
91
|
+
if (baseSecSchemes[name]) {
|
|
92
|
+
// If both have the same security scheme name, check if they're identical
|
|
93
|
+
const baseScheme = JSON.stringify(baseSecSchemes[name]);
|
|
94
|
+
const otherScheme = JSON.stringify(scheme);
|
|
95
|
+
if (baseScheme !== otherScheme) {
|
|
96
|
+
throw new Error(`Security scheme conflict: "${name}" differs between specs`);
|
|
97
|
+
}
|
|
98
|
+
// Same scheme — skip silently
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
merged.components.securitySchemes[name] = scheme;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Merge other component types if present
|
|
105
|
+
const componentTypes = [
|
|
106
|
+
'parameters',
|
|
107
|
+
'requestBodies',
|
|
108
|
+
'responses',
|
|
109
|
+
'headers',
|
|
110
|
+
'examples',
|
|
111
|
+
'links',
|
|
112
|
+
'callbacks',
|
|
113
|
+
];
|
|
114
|
+
for (const compType of componentTypes) {
|
|
115
|
+
const baseComp = base.components?.[compType];
|
|
116
|
+
const otherComp = other.components?.[compType];
|
|
117
|
+
if (otherComp) {
|
|
118
|
+
if (!baseComp) {
|
|
119
|
+
merged.components[compType] = { ...otherComp };
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
for (const [name, value] of Object.entries(otherComp)) {
|
|
123
|
+
if (baseComp[name]) {
|
|
124
|
+
throw new Error(`Component conflict in ${compType}: "${name}" exists in both specs`);
|
|
125
|
+
}
|
|
126
|
+
merged.components[compType][name] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Merge tags (deduplicate by name)
|
|
132
|
+
if (base.tags || other.tags) {
|
|
133
|
+
const tagMap = new Map();
|
|
134
|
+
for (const tag of base.tags ?? []) {
|
|
135
|
+
tagMap.set(tag.name, tag);
|
|
136
|
+
}
|
|
137
|
+
for (const tag of other.tags ?? []) {
|
|
138
|
+
if (!tagMap.has(tag.name)) {
|
|
139
|
+
tagMap.set(tag.name, tag);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
merged.tags = [...tagMap.values()];
|
|
143
|
+
}
|
|
144
|
+
// Merge top-level security (union)
|
|
145
|
+
if (base.security || other.security) {
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
const mergedSecurity = [];
|
|
148
|
+
for (const req of [...(base.security ?? []), ...(other.security ?? [])]) {
|
|
149
|
+
const key = JSON.stringify(req);
|
|
150
|
+
if (!seen.has(key)) {
|
|
151
|
+
seen.add(key);
|
|
152
|
+
mergedSecurity.push(req);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
merged.security = mergedSecurity;
|
|
156
|
+
}
|
|
157
|
+
const pathCount = Object.keys(merged.paths).length;
|
|
158
|
+
const schemaCount = Object.keys(merged.components?.schemas ?? {}).length;
|
|
159
|
+
logger.info(`Merged result: ${pathCount} paths, ${schemaCount} schemas`);
|
|
160
|
+
return merged;
|
|
161
|
+
}
|