wizards 0.1.9
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/LICENSE +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mcpListTools } from "../invoke/mcp-client.js";
|
|
2
|
+
import { classifyMcpTool } from "./classify.js";
|
|
3
|
+
import { MAX_ACTIONS_PER_CONNECTOR } from "./normalize-openapi.js";
|
|
4
|
+
function humanizeToolName(name) {
|
|
5
|
+
const words = name
|
|
6
|
+
.replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
|
|
7
|
+
.replace(/[_\-.]+/gu, " ")
|
|
8
|
+
.trim();
|
|
9
|
+
return words ? words[0].toUpperCase() + words.slice(1).toLowerCase() : name;
|
|
10
|
+
}
|
|
11
|
+
/** MCP server tools/list → normalized actions. */
|
|
12
|
+
export async function normalizeMcpServer(params) {
|
|
13
|
+
const tools = await mcpListTools(params);
|
|
14
|
+
const actions = [];
|
|
15
|
+
const skipped = [];
|
|
16
|
+
const seenIds = new Set();
|
|
17
|
+
let droppedCount = 0;
|
|
18
|
+
for (const tool of tools) {
|
|
19
|
+
if (actions.length >= MAX_ACTIONS_PER_CONNECTOR) {
|
|
20
|
+
droppedCount += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const id = tool.name
|
|
24
|
+
.replace(/[^a-zA-Z0-9]+/gu, "_")
|
|
25
|
+
.replace(/^_+|_+$/gu, "")
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.slice(0, 60);
|
|
28
|
+
if (!id || seenIds.has(id)) {
|
|
29
|
+
skipped.push({
|
|
30
|
+
id: tool.name,
|
|
31
|
+
reason: id ? "duplicate tool name" : "unusable tool name",
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
seenIds.add(id);
|
|
36
|
+
// Humanize bare tool names ("read_wiki_structure" → "Read wiki structure")
|
|
37
|
+
// so catalogs, the permissions matrix, and approval cards read naturally.
|
|
38
|
+
const summary = tool.title ?? humanizeToolName(tool.name);
|
|
39
|
+
actions.push({
|
|
40
|
+
classification: classifyMcpTool(tool.annotations ?? {}),
|
|
41
|
+
description: tool.description ?? summary,
|
|
42
|
+
id,
|
|
43
|
+
input_json_schema: tool.inputSchema ?? {
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
properties: {},
|
|
46
|
+
type: "object",
|
|
47
|
+
},
|
|
48
|
+
invoke: { kind: "mcp", tool_name: tool.name },
|
|
49
|
+
summary,
|
|
50
|
+
tags: [],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
actions,
|
|
55
|
+
applied_overrides: 0,
|
|
56
|
+
base_url: null,
|
|
57
|
+
description: null,
|
|
58
|
+
dropped_count: droppedCount,
|
|
59
|
+
security_schemes: null,
|
|
60
|
+
skipped,
|
|
61
|
+
title: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ImportValidationError } from "../errors.js";
|
|
2
|
+
import { classifyHttpOperation } from "./classify.js";
|
|
3
|
+
/**
|
|
4
|
+
* OpenAPI spec text → normalized actions, via the executor openapi core
|
|
5
|
+
* (`parse` + `extract` handle $refs, parameter styles, and merge path/query/
|
|
6
|
+
* header/body params into one JSON-Schema input with the body under `body`).
|
|
7
|
+
*
|
|
8
|
+
* The executor/effect packages are imported LAZILY: they run ONLY at admin
|
|
9
|
+
* import/refresh time, never at plugin boot or action execute time — the
|
|
10
|
+
* stored `NormalizedAction[]` is what boot and execute consume. (Same
|
|
11
|
+
* lazy-import posture the AI app uses for heavyweight optional deps.)
|
|
12
|
+
*
|
|
13
|
+
* Registry `specOverrides` are RFC 6902 patches the registry publishes to
|
|
14
|
+
* correct a published spec (Figma's truncated OAuth scopes, say). They are
|
|
15
|
+
* applied to the parsed document *before* extraction, and a patch that does
|
|
16
|
+
* not apply fails the import instead of silently normalizing the unpatched
|
|
17
|
+
* spec.
|
|
18
|
+
*/
|
|
19
|
+
/** Hard cap per connector; drops are counted and surfaced, never silent. */
|
|
20
|
+
export const MAX_ACTIONS_PER_CONNECTOR = 500;
|
|
21
|
+
/** Spec size ceiling, checked before the body is read and again after. */
|
|
22
|
+
export const MAX_SPEC_BYTES = 5 * 1024 * 1024;
|
|
23
|
+
function snakeCaseId(raw) {
|
|
24
|
+
return raw
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
|
|
26
|
+
.replace(/[^a-zA-Z0-9]+/gu, "_")
|
|
27
|
+
.replace(/^_+|_+$/gu, "")
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.slice(0, 60);
|
|
30
|
+
}
|
|
31
|
+
export async function normalizeOpenApiSpec(specText, options) {
|
|
32
|
+
if (Buffer.byteLength(specText, "utf8") > MAX_SPEC_BYTES) {
|
|
33
|
+
throw new ImportValidationError(`spec too large (> ${MAX_SPEC_BYTES / (1024 * 1024)} MB); import a smaller spec or use a filtered variant`);
|
|
34
|
+
}
|
|
35
|
+
const overrides = options?.specOverrides ?? [];
|
|
36
|
+
const [{ extract, parse }, { Effect, Option }] = await Promise.all([
|
|
37
|
+
import("@executor-js/plugin-openapi/core"),
|
|
38
|
+
import("effect"),
|
|
39
|
+
]);
|
|
40
|
+
const optional = (value) => Option.getOrUndefined(value);
|
|
41
|
+
const doc = await Effect.runPromise(parse(specText));
|
|
42
|
+
if (overrides.length > 0) {
|
|
43
|
+
const { applyPatch } = await import("rfc6902");
|
|
44
|
+
const failures = applyPatch(doc, overrides).filter((error) => error !== null);
|
|
45
|
+
if (failures.length > 0) {
|
|
46
|
+
throw new ImportValidationError(`registry spec overrides do not apply to this spec: ${failures
|
|
47
|
+
.map((error) => error.message)
|
|
48
|
+
.join("; ")}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const extracted = await Effect.runPromise(extract(doc));
|
|
52
|
+
/** Substitute server-URL variables with their declared defaults. */
|
|
53
|
+
const resolveServerBaseUrl = () => {
|
|
54
|
+
const server = extracted.servers[0];
|
|
55
|
+
if (!server) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const variables = optional(server.variables) ?? {};
|
|
59
|
+
const url = server.url.replace(/\{([^}]+)\}/gu, (match, name) => variables[name]?.default ?? match);
|
|
60
|
+
return url.replace(/\/+$/u, "");
|
|
61
|
+
};
|
|
62
|
+
const actions = [];
|
|
63
|
+
const skipped = [];
|
|
64
|
+
const seenIds = new Set();
|
|
65
|
+
let droppedCount = 0;
|
|
66
|
+
for (const op of extracted.operations) {
|
|
67
|
+
if (op.deprecated) {
|
|
68
|
+
skipped.push({ id: op.operationId, reason: "deprecated" });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (actions.length >= MAX_ACTIONS_PER_CONNECTOR) {
|
|
72
|
+
droppedCount += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let id = snakeCaseId(op.operationId);
|
|
76
|
+
if (!id) {
|
|
77
|
+
id = snakeCaseId(`${op.method}_${op.pathTemplate}`);
|
|
78
|
+
}
|
|
79
|
+
if (!id) {
|
|
80
|
+
skipped.push({ id: op.operationId, reason: "unusable operation id" });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
while (seenIds.has(id)) {
|
|
84
|
+
id = `${id}_${op.method.toLowerCase()}`.slice(0, 60);
|
|
85
|
+
if (seenIds.has(id)) {
|
|
86
|
+
id = `${id}2`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
seenIds.add(id);
|
|
90
|
+
const inputSchema = optional(op.inputSchema);
|
|
91
|
+
const requestBody = optional(op.requestBody);
|
|
92
|
+
const summary = optional(op.summary) ??
|
|
93
|
+
`${op.method.toUpperCase()} ${op.pathTemplate}`;
|
|
94
|
+
actions.push({
|
|
95
|
+
classification: classifyHttpOperation(op.method, op.pathTemplate),
|
|
96
|
+
description: optional(op.description) ?? summary,
|
|
97
|
+
id,
|
|
98
|
+
input_json_schema: inputSchema ?? {
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
properties: {},
|
|
101
|
+
type: "object",
|
|
102
|
+
},
|
|
103
|
+
invoke: {
|
|
104
|
+
body_content_type: requestBody?.contentType ?? null,
|
|
105
|
+
kind: "http",
|
|
106
|
+
method: op.method,
|
|
107
|
+
params: op.parameters.map((param) => ({
|
|
108
|
+
location: param.location,
|
|
109
|
+
name: param.name,
|
|
110
|
+
required: param.required,
|
|
111
|
+
})),
|
|
112
|
+
path_template: op.pathTemplate,
|
|
113
|
+
},
|
|
114
|
+
summary,
|
|
115
|
+
tags: [...op.tags],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const securitySchemes = doc
|
|
119
|
+
.components?.securitySchemes ?? null;
|
|
120
|
+
return {
|
|
121
|
+
actions,
|
|
122
|
+
applied_overrides: overrides.length,
|
|
123
|
+
base_url: resolveServerBaseUrl(),
|
|
124
|
+
description: optional(extracted.description) ?? null,
|
|
125
|
+
dropped_count: droppedCount,
|
|
126
|
+
security_schemes: securitySchemes,
|
|
127
|
+
skipped,
|
|
128
|
+
title: optional(extracted.title) ?? null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createGuardedFetch } from "../net/guarded-fetch.js";
|
|
2
|
+
/**
|
|
3
|
+
* Execute an imported HTTP action: substitute path params, place query/header
|
|
4
|
+
* params, serialize the JSON body, inject credentials per the stored auth
|
|
5
|
+
* config, and clamp the response for model context. Deliberately small and
|
|
6
|
+
* fetch-based — the executor request pipeline stays confined to import time.
|
|
7
|
+
*
|
|
8
|
+
* `base_url` came from a spec fetched over the network, so the request goes
|
|
9
|
+
* through the SSRF guard on every hop like the import-time fetches do.
|
|
10
|
+
*/
|
|
11
|
+
/** Response clamp: tool output feeds model context; oversize bodies truncate. */
|
|
12
|
+
export const MAX_RESPONSE_CHARS = 200_000;
|
|
13
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
14
|
+
export class ExternalActionError extends Error {
|
|
15
|
+
body;
|
|
16
|
+
status;
|
|
17
|
+
constructor(message, status, body) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ExternalActionError";
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.body = body;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function renderTemplate(template, credentials) {
|
|
25
|
+
return template.replace(/\{\{\s*([a-z0-9_]+)\s*\}\}/giu, (_m, key) => credentials[key] ?? "");
|
|
26
|
+
}
|
|
27
|
+
/** Credential placement per auth kind; oauth2 bearer uses the fresh token. */
|
|
28
|
+
function applyAuth(params) {
|
|
29
|
+
const { accessToken, auth, headers, url } = params;
|
|
30
|
+
if (auth.kind === "oauth2") {
|
|
31
|
+
headers.set("authorization", `Bearer ${accessToken}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (auth.kind === "api_key") {
|
|
35
|
+
let credentials = {};
|
|
36
|
+
try {
|
|
37
|
+
credentials = JSON.parse(accessToken);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new ExternalActionError("stored credentials are not valid JSON — reconnect the connection", null, null);
|
|
41
|
+
}
|
|
42
|
+
const value = renderTemplate(auth.placement.value_template, credentials);
|
|
43
|
+
if (auth.placement.in === "query") {
|
|
44
|
+
url.searchParams.set(auth.placement.name, value);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
headers.set(auth.placement.name, value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function buildHttpRequest(params) {
|
|
52
|
+
const { accessToken, auth, baseUrl, input, invoke } = params;
|
|
53
|
+
let path = invoke.path_template;
|
|
54
|
+
const headers = new Headers({ accept: "application/json" });
|
|
55
|
+
// Set before operation params and auth so neither is silently overwritten.
|
|
56
|
+
for (const header of params.requiredHeaders ?? []) {
|
|
57
|
+
headers.set(header.name, header.value);
|
|
58
|
+
}
|
|
59
|
+
const query = [];
|
|
60
|
+
for (const param of invoke.params) {
|
|
61
|
+
const value = input[param.name];
|
|
62
|
+
if (value === undefined || value === null) {
|
|
63
|
+
if (param.required && param.location === "path") {
|
|
64
|
+
throw new ExternalActionError(`missing required path parameter "${param.name}"`, null, null);
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (param.location === "path") {
|
|
69
|
+
path = path.replaceAll(`{${param.name}}`, encodeURIComponent(String(value)));
|
|
70
|
+
}
|
|
71
|
+
else if (param.location === "query") {
|
|
72
|
+
query.push([param.name, value]);
|
|
73
|
+
}
|
|
74
|
+
else if (param.location === "header") {
|
|
75
|
+
headers.set(param.name, String(value));
|
|
76
|
+
}
|
|
77
|
+
// cookie params are unsupported in v1 — ignored by design.
|
|
78
|
+
}
|
|
79
|
+
const url = new URL(baseUrl.replace(/\/+$/u, "") + path);
|
|
80
|
+
for (const [name, value] of query) {
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (const item of value) {
|
|
83
|
+
url.searchParams.append(name, String(item));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
url.searchParams.append(name, String(value));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let body;
|
|
91
|
+
if (invoke.body_content_type && input.body !== undefined) {
|
|
92
|
+
headers.set("content-type", invoke.body_content_type);
|
|
93
|
+
body =
|
|
94
|
+
invoke.body_content_type.includes("json") ||
|
|
95
|
+
typeof input.body !== "string"
|
|
96
|
+
? JSON.stringify(input.body)
|
|
97
|
+
: input.body;
|
|
98
|
+
}
|
|
99
|
+
applyAuth({ accessToken, auth, headers, url });
|
|
100
|
+
return { body, headers, method: invoke.method.toUpperCase(), url };
|
|
101
|
+
}
|
|
102
|
+
export async function executeHttpAction(params) {
|
|
103
|
+
const { accessToken, input, invoke, record } = params;
|
|
104
|
+
const fetchImpl = createGuardedFetch({
|
|
105
|
+
fetchImpl: params.fetchImpl,
|
|
106
|
+
...(params.lookupImpl ? { lookupImpl: params.lookupImpl } : {}),
|
|
107
|
+
});
|
|
108
|
+
if (!record.base_url) {
|
|
109
|
+
throw new ExternalActionError("imported connector has no base URL — set one via refresh/import", null, null);
|
|
110
|
+
}
|
|
111
|
+
const request = buildHttpRequest({
|
|
112
|
+
accessToken,
|
|
113
|
+
auth: record.auth_config,
|
|
114
|
+
baseUrl: record.base_url,
|
|
115
|
+
input,
|
|
116
|
+
invoke,
|
|
117
|
+
requiredHeaders: record.required_headers,
|
|
118
|
+
});
|
|
119
|
+
const response = await fetchImpl(request.url, {
|
|
120
|
+
body: request.body,
|
|
121
|
+
headers: request.headers,
|
|
122
|
+
method: request.method,
|
|
123
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
124
|
+
});
|
|
125
|
+
const text = await response.text();
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new ExternalActionError(`provider request failed (${response.status} ${request.method} ${request.url.pathname})`, response.status, text.slice(0, 2000));
|
|
128
|
+
}
|
|
129
|
+
const truncated = text.length > MAX_RESPONSE_CHARS;
|
|
130
|
+
const clamped = truncated ? text.slice(0, MAX_RESPONSE_CHARS) : text;
|
|
131
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
132
|
+
if (contentType.includes("json")) {
|
|
133
|
+
try {
|
|
134
|
+
return truncated
|
|
135
|
+
? { body: clamped, truncated: true }
|
|
136
|
+
: JSON.parse(clamped);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { body: clamped, truncated };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { body: clamped, content_type: contentType || null, truncated };
|
|
143
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createGuardedFetch } from "../net/guarded-fetch.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adapter over the official MCP client (`@modelcontextprotocol/client`) for
|
|
5
|
+
* imported MCP connectors. It owns three things and nothing else:
|
|
6
|
+
*
|
|
7
|
+
* - **transport choice** — streamable HTTP by default; SSE only when the
|
|
8
|
+
* registry declares that transport or an admin picked it for a legacy URL,
|
|
9
|
+
* and as the fallback when a server rejects the streamable handshake;
|
|
10
|
+
* - **credentials** — Connections resolves the connection's token and this
|
|
11
|
+
* module turns it into request headers; the SDK's own OAuth provider is
|
|
12
|
+
* deliberately not used, since grants live in the connections store;
|
|
13
|
+
* - **session lifetime** — every call opens a client and releases it in
|
|
14
|
+
* `finally`: the streamable transport's session is terminated (DELETE) and
|
|
15
|
+
* the client closed, so a failed `tools/call` never leaks a server-side
|
|
16
|
+
* session.
|
|
17
|
+
*
|
|
18
|
+
* Every request the SDK makes goes through the injected guarded fetch, so an
|
|
19
|
+
* MCP endpoint cannot be pointed at a private address.
|
|
20
|
+
*/
|
|
21
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
22
|
+
const CLIENT_INFO = {
|
|
23
|
+
name: "engenty-connections-external",
|
|
24
|
+
version: "0.0.1",
|
|
25
|
+
};
|
|
26
|
+
export const mcpToolSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
annotations: z
|
|
29
|
+
.object({
|
|
30
|
+
destructiveHint: z.boolean().optional(),
|
|
31
|
+
readOnlyHint: z.boolean().optional(),
|
|
32
|
+
})
|
|
33
|
+
.loose()
|
|
34
|
+
.nullish(),
|
|
35
|
+
description: z.string().nullish(),
|
|
36
|
+
inputSchema: z.record(z.string(), z.unknown()).nullish(),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
title: z.string().nullish(),
|
|
39
|
+
})
|
|
40
|
+
.loose();
|
|
41
|
+
export class McpRequestError extends Error {
|
|
42
|
+
status;
|
|
43
|
+
constructor(message, status = null) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "McpRequestError";
|
|
46
|
+
this.status = status;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function httpStatusFromUnknown(error) {
|
|
50
|
+
if (error instanceof McpRequestError && error.status !== null) {
|
|
51
|
+
return error.status;
|
|
52
|
+
}
|
|
53
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
54
|
+
const code = error.code;
|
|
55
|
+
if (typeof code === "number" && code >= 400 && code < 600) {
|
|
56
|
+
return code;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
const http = message.match(/\bHTTP\s+(\d{3})\b/iu);
|
|
61
|
+
if (http) {
|
|
62
|
+
return Number(http[1]);
|
|
63
|
+
}
|
|
64
|
+
const unauthorized = message.match(/\b(401)\b/u);
|
|
65
|
+
return unauthorized ? Number(unauthorized[1]) : null;
|
|
66
|
+
}
|
|
67
|
+
/** Anonymous MCP handshake / tools/list was refused as unauthorized. */
|
|
68
|
+
export function isUnauthorizedMcpError(error) {
|
|
69
|
+
if (httpStatusFromUnknown(error) === 401) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return /\b401\b|unauthorized/iu.test(message);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Transport factories are resolved lazily so the SDK (and its OAuth/JOSE
|
|
77
|
+
* dependency tree) is loaded only when an MCP connector actually runs.
|
|
78
|
+
*/
|
|
79
|
+
async function connectClient(params, transport) {
|
|
80
|
+
const sdk = await import("@modelcontextprotocol/client");
|
|
81
|
+
const url = new URL(params.endpoint);
|
|
82
|
+
const options = {
|
|
83
|
+
fetch: params.fetchImpl ?? createGuardedFetch(),
|
|
84
|
+
requestInit: params.headers ? { headers: params.headers } : undefined,
|
|
85
|
+
};
|
|
86
|
+
const clientTransport = transport === "sse"
|
|
87
|
+
? new sdk.SSEClientTransport(url, options)
|
|
88
|
+
: new sdk.StreamableHTTPClientTransport(url, options);
|
|
89
|
+
const client = new sdk.Client(CLIENT_INFO);
|
|
90
|
+
await client.connect(clientTransport);
|
|
91
|
+
return { client, transport: clientTransport };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Run `fn` against a connected client, closing the session in `finally`.
|
|
95
|
+
* A streamable-HTTP server that refuses the handshake is retried once over
|
|
96
|
+
* SSE — the legacy transport many hosted servers still answer on.
|
|
97
|
+
*/
|
|
98
|
+
async function withMcpClient(params, fn) {
|
|
99
|
+
const declared = params.transport ?? "streamable-http";
|
|
100
|
+
const attempts = declared === "sse" ? ["sse"] : ["streamable-http", "sse"];
|
|
101
|
+
let lastError;
|
|
102
|
+
for (const transport of attempts) {
|
|
103
|
+
let session;
|
|
104
|
+
try {
|
|
105
|
+
session = await connectClient(params, transport);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
lastError = error;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return await fn(session.client);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new McpRequestError(error instanceof Error ? error.message : String(error), httpStatusFromUnknown(error));
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
// Best effort in both steps: the session is already gone if either
|
|
119
|
+
// fails, and neither may mask the caller's own error.
|
|
120
|
+
await session.transport.terminateSession?.().catch(() => {
|
|
121
|
+
// Server does not allow client-side termination.
|
|
122
|
+
});
|
|
123
|
+
await session.client.close().catch(() => {
|
|
124
|
+
// Already closed.
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw new McpRequestError(`cannot connect to MCP server ${params.endpoint}: ${lastError instanceof Error ? lastError.message : String(lastError)}`, httpStatusFromUnknown(lastError));
|
|
129
|
+
}
|
|
130
|
+
export async function mcpListTools(params) {
|
|
131
|
+
return await withMcpClient(params, async (client) => {
|
|
132
|
+
const result = await client.listTools(undefined, {
|
|
133
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
134
|
+
});
|
|
135
|
+
return z
|
|
136
|
+
.object({ tools: z.array(mcpToolSchema).default([]) })
|
|
137
|
+
.loose()
|
|
138
|
+
.parse(result ?? {}).tools;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export async function mcpCallTool(params) {
|
|
142
|
+
return await withMcpClient(params, (client) => client.callTool({ arguments: params.args, name: params.toolName }, { timeout: REQUEST_TIMEOUT_MS }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { assertPublicHttpHost } from "../../shims/web-ingest.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every network call this module makes reaches a URL that came from the
|
|
4
|
+
* registry, an admin-pasted field, or a stored import record — attacker
|
|
5
|
+
* influenceable in all three cases. `createGuardedFetch` is the single way out:
|
|
6
|
+
* it validates the host (DNS-resolved, private/link-local ranges blocked)
|
|
7
|
+
* before the first request and again before following each redirect.
|
|
8
|
+
*
|
|
9
|
+
* `safeFetchFollowingRedirects` in `@engenty/web-ingest` covers GET/HEAD only;
|
|
10
|
+
* the MCP transport POSTs JSON-RPC bodies and DELETEs sessions, so the redirect
|
|
11
|
+
* chain is walked here with method and body carried the way the HTTP spec
|
|
12
|
+
* prescribes (307/308 replay, 301/302/303 downgrade to GET).
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
15
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
16
|
+
/** Redirects that must not replay the original method and body. */
|
|
17
|
+
const DOWNGRADE_TO_GET = new Set([301, 302, 303]);
|
|
18
|
+
/** A `fetch` drop-in that refuses private/non-HTTP targets on every hop. */
|
|
19
|
+
export function createGuardedFetch(options) {
|
|
20
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
21
|
+
const maxRedirects = options?.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
22
|
+
return (async (input, init) => {
|
|
23
|
+
let currentUrl = typeof input === "string" ? input : String(input);
|
|
24
|
+
let currentInit = { ...init, redirect: "manual" };
|
|
25
|
+
let hops = 0;
|
|
26
|
+
await assertPublicHttpHost(currentUrl, options?.lookupImpl);
|
|
27
|
+
while (true) {
|
|
28
|
+
const response = await fetchImpl(currentUrl, currentInit);
|
|
29
|
+
if (!REDIRECT_STATUSES.has(response.status)) {
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
hops += 1;
|
|
33
|
+
if (hops > maxRedirects) {
|
|
34
|
+
throw new Error(`Too many redirects (max ${maxRedirects}) following ${currentUrl}`);
|
|
35
|
+
}
|
|
36
|
+
const location = response.headers.get("location");
|
|
37
|
+
if (!location?.trim()) {
|
|
38
|
+
throw new Error(`Redirect response (HTTP ${response.status}) missing Location header`);
|
|
39
|
+
}
|
|
40
|
+
let nextUrl;
|
|
41
|
+
try {
|
|
42
|
+
nextUrl = new URL(location, currentUrl).toString();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error(`Malformed Location header in redirect: ${location}`);
|
|
46
|
+
}
|
|
47
|
+
await assertPublicHttpHost(nextUrl, options?.lookupImpl);
|
|
48
|
+
if (DOWNGRADE_TO_GET.has(response.status)) {
|
|
49
|
+
currentInit = { ...currentInit, body: undefined, method: "GET" };
|
|
50
|
+
}
|
|
51
|
+
else if (currentInit.body !== undefined &&
|
|
52
|
+
currentInit.body !== null &&
|
|
53
|
+
typeof currentInit.body !== "string") {
|
|
54
|
+
// A stream body cannot be replayed; refuse rather than send a
|
|
55
|
+
// truncated request to a host we were redirected to.
|
|
56
|
+
throw new Error(`Cannot follow HTTP ${response.status} redirect: request body is not replayable`);
|
|
57
|
+
}
|
|
58
|
+
currentUrl = nextUrl;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export class SpecTooLargeError extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "SpecTooLargeError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* GET a text document through the guard, refusing oversize payloads *before*
|
|
70
|
+
* reading them: `content-length` is checked first, then the body is consumed
|
|
71
|
+
* chunk by chunk and abandoned the moment it crosses `maxBytes`.
|
|
72
|
+
*/
|
|
73
|
+
export async function fetchTextBounded(params) {
|
|
74
|
+
const response = await params.fetchImpl(params.url, {
|
|
75
|
+
...(params.headers ? { headers: params.headers } : {}),
|
|
76
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`spec fetch failed (${response.status}): ${params.url}`);
|
|
80
|
+
}
|
|
81
|
+
const declared = Number(response.headers.get("content-length"));
|
|
82
|
+
if (Number.isFinite(declared) && declared > params.maxBytes) {
|
|
83
|
+
throw new SpecTooLargeError(`spec too large (${declared} bytes > ${params.maxBytes}); import a smaller spec or use a filtered variant`);
|
|
84
|
+
}
|
|
85
|
+
const body = response.body;
|
|
86
|
+
if (!body) {
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
if (Buffer.byteLength(text, "utf8") > params.maxBytes) {
|
|
89
|
+
throw new SpecTooLargeError(`spec too large (> ${params.maxBytes} bytes); import a smaller spec or use a filtered variant`);
|
|
90
|
+
}
|
|
91
|
+
return text;
|
|
92
|
+
}
|
|
93
|
+
const reader = body.getReader();
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let total = 0;
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (!value) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
total += value.byteLength;
|
|
106
|
+
if (total > params.maxBytes) {
|
|
107
|
+
throw new SpecTooLargeError(`spec too large (> ${params.maxBytes} bytes); import a smaller spec or use a filtered variant`);
|
|
108
|
+
}
|
|
109
|
+
chunks.push(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
reader.releaseLock();
|
|
114
|
+
await body.cancel().catch(() => {
|
|
115
|
+
// Already drained or errored — nothing to release.
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
119
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createGuardedFetch } from "./net/guarded-fetch.js";
|
|
2
|
+
/**
|
|
3
|
+
* RFC 7591 dynamic client registration for imported OAuth MCP servers.
|
|
4
|
+
* The MCP SDK's own OAuth provider is unused — the registered client is
|
|
5
|
+
* stored on the import row and the token still lands in the connections store.
|
|
6
|
+
*
|
|
7
|
+
* Prefer a public client (`token_endpoint_auth_method: "none"`, no secret) so
|
|
8
|
+
* Figma-style MCP authorization servers accept registration; fall back to a
|
|
9
|
+
* confidential `client_secret_post` client for servers that require it.
|
|
10
|
+
*/
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 20_000;
|
|
12
|
+
const AUTH_METHODS = ["none", "client_secret_post"];
|
|
13
|
+
export function connectionsRedirectUri() {
|
|
14
|
+
const override = process.env.CONNECTIONS_REDIRECT_URI?.trim();
|
|
15
|
+
if (override) {
|
|
16
|
+
return override;
|
|
17
|
+
}
|
|
18
|
+
const base = process.env.ENGENTY_API_BASE_URL?.trim()?.replace(/\/$/u, "");
|
|
19
|
+
if (!base) {
|
|
20
|
+
throw new Error("Missing ENGENTY_API_BASE_URL (run pnpm portless:env:sync)");
|
|
21
|
+
}
|
|
22
|
+
return `${base}/api/connections/oauth/callback`;
|
|
23
|
+
}
|
|
24
|
+
function registrationErrorMessage(status, data, rawBody) {
|
|
25
|
+
const fromJson = data.error_description?.trim() ||
|
|
26
|
+
data.error?.trim() ||
|
|
27
|
+
(typeof data === "object" && data && "message" in data
|
|
28
|
+
? String(data.message ?? "").trim()
|
|
29
|
+
: "");
|
|
30
|
+
const fromBody = rawBody.trim().slice(0, 300);
|
|
31
|
+
const detail = fromJson || fromBody || "no error body";
|
|
32
|
+
return `dynamic client registration failed (${status}): ${detail}`;
|
|
33
|
+
}
|
|
34
|
+
async function postRegistration(params) {
|
|
35
|
+
return params.fetchImpl(params.registrationEndpoint, {
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
client_name: params.clientName,
|
|
38
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
39
|
+
redirect_uris: [params.redirectUri],
|
|
40
|
+
response_types: ["code"],
|
|
41
|
+
scope: params.scopes.join(" "),
|
|
42
|
+
token_endpoint_auth_method: params.authMethod,
|
|
43
|
+
}),
|
|
44
|
+
headers: {
|
|
45
|
+
accept: "application/json",
|
|
46
|
+
"content-type": "application/json",
|
|
47
|
+
},
|
|
48
|
+
method: "POST",
|
|
49
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function registerDynamicClient(params) {
|
|
53
|
+
const fetchImpl = params.fetchImpl ?? createGuardedFetch();
|
|
54
|
+
const errors = [];
|
|
55
|
+
for (const authMethod of AUTH_METHODS) {
|
|
56
|
+
const response = await postRegistration({
|
|
57
|
+
authMethod,
|
|
58
|
+
clientName: params.clientName,
|
|
59
|
+
fetchImpl,
|
|
60
|
+
redirectUri: params.redirectUri,
|
|
61
|
+
registrationEndpoint: params.registrationEndpoint,
|
|
62
|
+
scopes: params.scopes,
|
|
63
|
+
});
|
|
64
|
+
const rawBody = await response.text();
|
|
65
|
+
const data = (() => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(rawBody);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
errors.push(registrationErrorMessage(response.status, data, rawBody));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!data.client_id?.trim()) {
|
|
78
|
+
errors.push(`dynamic client registration returned no client_id (${authMethod})`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Public clients (`none`) omit client_secret; confidential ones return it.
|
|
82
|
+
return {
|
|
83
|
+
clientId: data.client_id,
|
|
84
|
+
clientSecret: data.client_secret ?? "",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
throw new Error(errors.at(-1) ?? "dynamic client registration failed");
|
|
88
|
+
}
|