@aifabrix/builder 2.53.0 → 2.54.1
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/bin/aifabrix.js +2 -2
- package/lib/api/assistants.api.js +8 -5
- package/lib/api/channel-identity.api.js +41 -0
- package/lib/api/evidence-authoring.api.js +5 -1
- package/lib/api/evidence.api.js +2 -0
- package/lib/api/types/evidence.types.js +2 -0
- package/lib/api/work-search.api.js +136 -0
- package/lib/channels/add-channel.js +163 -126
- package/lib/channels/approval-guides/chatgpt.js +14 -18
- package/lib/channels/approval-guides/claude.js +29 -0
- package/lib/channels/approval-guides/index.js +88 -22
- package/lib/channels/approval-guides/mcp-client.js +27 -0
- package/lib/channels/approval-guides/microsoft-copilot.js +52 -3
- package/lib/channels/approval-guides/slack.js +10 -10
- package/lib/channels/approval-guides/teams-bot.js +8 -12
- package/lib/channels/channel-add-continuations.js +91 -0
- package/lib/channels/channel-add-ensure.js +124 -27
- package/lib/channels/channel-add-existing.js +160 -28
- package/lib/channels/channel-add-failure.js +60 -0
- package/lib/channels/channel-add-remaining.js +486 -0
- package/lib/channels/channel-add-tty.js +190 -0
- package/lib/channels/channel-add-wizard-config.js +138 -0
- package/lib/channels/channel-add-wizard.js +250 -0
- package/lib/channels/channel-artifact-generator.js +27 -13
- package/lib/channels/channel-cardinality.js +83 -0
- package/lib/channels/channel-catalog-icon.js +45 -0
- package/lib/channels/channel-cli-instructions.js +75 -0
- package/lib/channels/channel-configuration-map.js +213 -0
- package/lib/channels/channel-credential.js +11 -14
- package/lib/channels/channel-definition-project.js +101 -0
- package/lib/channels/channel-definition-registry.js +154 -0
- package/lib/channels/channel-description.js +59 -2
- package/lib/channels/channel-discovery.js +16 -2
- package/lib/channels/channel-external-system.js +86 -9
- package/lib/channels/channel-install-envelope-actions.js +88 -0
- package/lib/channels/channel-install-envelope-microsoft.js +155 -0
- package/lib/channels/channel-install-envelope.js +356 -0
- package/lib/channels/channel-install-help-md.js +172 -0
- package/lib/channels/channel-install-http-payload.js +62 -0
- package/lib/channels/channel-install-inventory.js +115 -0
- package/lib/channels/channel-install-profiles.js +90 -0
- package/lib/channels/channel-instance-collision.js +118 -0
- package/lib/channels/channel-instance-key.js +100 -0
- package/lib/channels/channel-instance-materialize.js +240 -0
- package/lib/channels/channel-integration-generator.js +16 -9
- package/lib/channels/channel-integration-template.js +101 -22
- package/lib/channels/channel-inventory-fetch.js +79 -0
- package/lib/channels/channel-oauth-preflight.js +104 -0
- package/lib/channels/channel-package-version.js +76 -0
- package/lib/channels/channel-predefined-client.js +154 -0
- package/lib/channels/channel-probe.js +7 -2
- package/lib/channels/channel-runtime-resolver.js +180 -72
- package/lib/channels/channel-status-display.js +86 -16
- package/lib/channels/channel-targets.js +106 -92
- package/lib/channels/chatgpt-callback.js +318 -0
- package/lib/channels/chatgpt-channel-add.js +229 -0
- package/lib/channels/chatgpt-preflight.js +396 -0
- package/lib/channels/chatgpt-runtime-contract.js +17 -0
- package/lib/channels/chatgpt-setup-values.js +124 -0
- package/lib/channels/claude-callback.js +67 -0
- package/lib/channels/claude-channel-add.js +254 -0
- package/lib/channels/claude-runtime-contract.js +40 -0
- package/lib/channels/claude-runtime.js +58 -0
- package/lib/channels/claude-setup-values.js +89 -0
- package/lib/channels/mcp-client/config-merge.js +148 -0
- package/lib/channels/mcp-client/ensure-client.js +231 -0
- package/lib/channels/mcp-client/paths.js +53 -0
- package/lib/channels/mcp-client/profiles.js +122 -0
- package/lib/channels/mcp-client/redirect-uris.js +99 -0
- package/lib/channels/mcp-client/runtime.js +59 -0
- package/lib/channels/mcp-client-channel-add.js +186 -0
- package/lib/channels/microsoft-copilot-channel-add.js +117 -7
- package/lib/channels/microsoft-copilot-entra-identifier-uris.js +187 -0
- package/lib/channels/microsoft-copilot-entra-sso-form.js +106 -18
- package/lib/channels/microsoft-copilot-entra-sso.js +31 -2
- package/lib/channels/microsoft-copilot-package-prior.js +107 -0
- package/lib/channels/microsoft-copilot-package.js +29 -28
- package/lib/channels/microsoft-provisioning/a365.js +80 -0
- package/lib/channels/microsoft-provisioning/auth-config.js +240 -0
- package/lib/channels/microsoft-provisioning/auth.js +179 -0
- package/lib/channels/microsoft-provisioning/cli.js +71 -0
- package/lib/channels/microsoft-provisioning/constants.js +75 -0
- package/lib/channels/microsoft-provisioning/errors.js +117 -0
- package/lib/channels/microsoft-provisioning/index.js +55 -0
- package/lib/channels/microsoft-provisioning/install-atk.js +158 -0
- package/lib/channels/microsoft-provisioning/install-teams.js +115 -0
- package/lib/channels/microsoft-provisioning/login-portal-device.js +236 -0
- package/lib/channels/microsoft-provisioning/login-portal-parse.js +71 -0
- package/lib/channels/microsoft-provisioning/login-portal-teams.js +193 -0
- package/lib/channels/microsoft-provisioning/login-portal.js +230 -0
- package/lib/channels/microsoft-provisioning/persist.js +110 -0
- package/lib/channels/microsoft-provisioning/teams-bot-args.js +119 -0
- package/lib/channels/microsoft-provisioning/teams-bot-controller.js +123 -0
- package/lib/channels/microsoft-provisioning/teams-bot-parse.js +134 -0
- package/lib/channels/microsoft-provisioning/teams-bot-persist.js +113 -0
- package/lib/channels/microsoft-provisioning/teams-bot-provision.js +234 -0
- package/lib/channels/microsoft-provisioning/teams-bot-secrets.js +45 -0
- package/lib/channels/microsoft-provisioning/teams-cli.js +82 -0
- package/lib/channels/runtime-tool-catalog.js +5 -2
- package/lib/channels/slack-channel-add-portal.js +130 -0
- package/lib/channels/slack-channel-add-tty-auth.js +95 -0
- package/lib/channels/slack-channel-add.js +258 -0
- package/lib/channels/slack-provisioning/apps.js +181 -0
- package/lib/channels/slack-provisioning/auth-store.js +110 -0
- package/lib/channels/slack-provisioning/auth-ticket.js +185 -0
- package/lib/channels/slack-provisioning/auth.js +115 -0
- package/lib/channels/slack-provisioning/callbacks.js +41 -0
- package/lib/channels/slack-provisioning/cli.js +226 -0
- package/lib/channels/slack-provisioning/constants.js +37 -0
- package/lib/channels/slack-provisioning/credentials.js +169 -0
- package/lib/channels/slack-provisioning/curl-shim.js +132 -0
- package/lib/channels/slack-provisioning/developer-install.js +118 -0
- package/lib/channels/slack-provisioning/env-file.js +120 -0
- package/lib/channels/slack-provisioning/errors.js +47 -0
- package/lib/channels/slack-provisioning/go-ca-bundle.js +137 -0
- package/lib/channels/slack-provisioning/index.js +250 -0
- package/lib/channels/slack-provisioning/install-cli.js +323 -0
- package/lib/channels/slack-provisioning/manifest.js +89 -0
- package/lib/channels/slack-provisioning/oauth-lock.js +33 -0
- package/lib/channels/slack-provisioning/offer-cli.js +113 -0
- package/lib/channels/slack-provisioning/onboard-members.js +122 -0
- package/lib/channels/slack-provisioning/onboard.js +89 -0
- package/lib/channels/slack-provisioning/persist.js +95 -0
- package/lib/channels/slack-provisioning/remaining-urls.js +158 -0
- package/lib/channels/slack-provisioning/with-curl-on-path.js +48 -0
- package/lib/channels/teams-bot-channel-add.js +94 -0
- package/lib/channels/vendor-dataplane-url.js +249 -0
- package/lib/cli/setup-certification.help.js +2 -0
- package/lib/cli/setup-external-system-help.js +2 -0
- package/lib/cli/setup-external-system.js +1 -1
- package/lib/commands/channel-add-help.js +366 -0
- package/lib/commands/channel-add-setup.js +162 -80
- package/lib/commands/channel-configure.js +118 -0
- package/lib/commands/channel.js +10 -1
- package/lib/commands/governance-upload-external.js +6 -3
- package/lib/commands/lifecycle-command-action.js +2 -5
- package/lib/commands/lifecycle-external.js +93 -21
- package/lib/commands/repair-datasource-test-payload.js +4 -2
- package/lib/commands/repair-datasource-webhooks-sync-bridge.js +23 -2
- package/lib/commands/repair-datasource.js +3 -0
- package/lib/commands/repair-external-run.js +59 -25
- package/lib/commands/repair-openapi-rest-runtime.js +127 -0
- package/lib/commands/repair-orchestration-tag-merge.js +339 -0
- package/lib/commands/repair-rbac-catalog-defaults.js +96 -0
- package/lib/commands/repair-rbac-platform-preset.js +26 -15
- package/lib/commands/repair-rbac-reader-roles.js +8 -21
- package/lib/commands/repair-rbac.js +10 -34
- package/lib/commands/repair-system-permissions.js +24 -22
- package/lib/commands/role-assistant-test-output.js +14 -0
- package/lib/commands/role-assistant.js +59 -21
- package/lib/commands/test-e2e-external.js +148 -21
- package/lib/commands/upload-credential-push-log.js +2 -1
- package/lib/commands/upload-publish-context.js +1 -1
- package/lib/commands/upload-role-assistant-shell.js +124 -0
- package/lib/commands/upload-sidecar-followups.js +6 -3
- package/lib/commands/upload.js +18 -3
- package/lib/core/secrets-docker-env.js +4 -1
- package/lib/core/secrets-env-content.js +17 -3
- package/lib/datasource/capability/capability-manifest.js +0 -3
- package/lib/datasource/capability/copy-test-payload.js +3 -0
- package/lib/external-system/deploy-helpers.js +26 -1
- package/lib/external-system/deploy.js +10 -6
- package/lib/external-system/download-helpers.js +29 -2
- package/lib/external-system/download-role-assistant.js +83 -4
- package/lib/external-system/download.js +40 -30
- package/lib/external-system/sync-deploy-manifest.js +3 -2
- package/lib/external-system/upload-identity.js +42 -0
- package/lib/generator/index.js +19 -15
- package/lib/generator/split.js +13 -2
- package/lib/lifecycle/role-assistant-governance.js +146 -0
- package/lib/lifecycle/scenario-upload.js +181 -30
- package/lib/programmatic/builder-api-cors.js +143 -0
- package/lib/programmatic/builder-api-request.js +6 -0
- package/lib/programmatic/builder-api-route-dispatch.js +56 -42
- package/lib/programmatic/builder-api-server.js +8 -1
- package/lib/programmatic/capability-registry.js +54 -0
- package/lib/programmatic/channel-install-help-resolve.js +359 -0
- package/lib/programmatic/channel-install-session.js +218 -0
- package/lib/programmatic/help-content/channel-onboarding.md +2 -0
- package/lib/programmatic/help-content/cip-overview.md +3 -1
- package/lib/programmatic/http-raw-response.js +62 -0
- package/lib/programmatic/openapi-channel-install-bindings.js +179 -0
- package/lib/programmatic/openapi-channel-install-schemas.js +286 -0
- package/lib/programmatic/openapi-component-schemas.js +18 -1
- package/lib/programmatic/openapi-description-enricher.js +3 -1
- package/lib/programmatic/openapi-descriptions.js +74 -3
- package/lib/programmatic/openapi-envelope-examples.js +117 -0
- package/lib/programmatic/openapi-operation-bindings.js +4 -1
- package/lib/programmatic/openapi-schema-descriptions-channel.js +316 -0
- package/lib/programmatic/openapi-spec.js +12 -8
- package/lib/programmatic/platform-rbac-catalog.js +36 -18
- package/lib/programmatic/route-handler-map.js +51 -1
- package/lib/programmatic/route-handlers-context.js +2 -1
- package/lib/programmatic/run-auth-permissions.js +54 -0
- package/lib/programmatic/run-channel-catalog.js +196 -0
- package/lib/programmatic/run-channel-install-artifacts.js +137 -0
- package/lib/programmatic/run-channel-install-copilot.js +353 -0
- package/lib/programmatic/run-channel-install-options.js +232 -0
- package/lib/programmatic/run-channel-install-teams-bot.js +204 -0
- package/lib/programmatic/run-channel-install.js +346 -0
- package/lib/programmatic/run-channel-package.js +87 -7
- package/lib/programmatic/workspace-context.js +2 -0
- package/lib/role-assistant/evidence-sync.js +4 -2
- package/lib/role-assistant/evidence-validation-issues.js +51 -0
- package/lib/role-assistant/knowledge-sync.js +10 -4
- package/lib/role-assistant/settings.js +49 -2
- package/lib/role-assistant/test-cases-evaluate.js +30 -2
- package/lib/role-assistant/test-cases-evidence.js +175 -0
- package/lib/role-assistant/test-cases-search.js +229 -0
- package/lib/role-assistant/test-cases.js +37 -4
- package/lib/role-assistant/test-runner-search.js +149 -0
- package/lib/role-assistant/test-runner-workhub-helpers.js +40 -2
- package/lib/role-assistant/test-runner-workhub-soft.js +9 -3
- package/lib/role-assistant/test-runner-workhub.js +29 -11
- package/lib/role-assistant/test-runner.js +16 -1
- package/lib/schema/external-datasource.schema.json +119 -16
- package/lib/schema/external-system.schema.json +159 -2
- package/lib/schema/mcp-client-profile.schema.json +103 -0
- package/lib/utils/app-path-resolve.js +4 -0
- package/lib/utils/authentication-env-template.js +44 -6
- package/lib/utils/credential-secrets-env.js +6 -1
- package/lib/utils/env-copy.js +19 -7
- package/lib/utils/external-env-template.js +20 -2
- package/lib/utils/paths.js +18 -1
- package/lib/utils/platform-shell-package.js +69 -0
- package/lib/utils/port-resolver.js +50 -1
- package/lib/utils/rewrite-name-flag.js +20 -0
- package/lib/utils/role-assistant-paths.js +13 -10
- package/lib/utils/schema-resolver.js +11 -1
- package/lib/utils/secrets-helpers.js +12 -21
- package/lib/utils/token-manager.js +10 -5
- package/lib/validation/validate.js +40 -0
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/applications/builder-api/Dockerfile +1 -1
- package/templates/applications/builder-api/env.template +7 -0
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +2 -2
- package/templates/applications/dataplane/rbac.yaml +7 -2
- package/templates/applications/keycloak/Dockerfile +30 -4
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +896 -0
- package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +19 -0
- package/templates/applications/keycloak/env.template +3 -0
- package/templates/applications/miso-controller/application.yaml +1 -2
- package/templates/applications/miso-controller/env.template +2 -0
- package/templates/channels/README.md +5 -5
- package/templates/channels/chatgpt/TROUBLESHOOTING.md +51 -0
- package/templates/channels/chatgpt/channel.json +59 -0
- package/templates/channels/chatgpt/icon.svg +1 -0
- package/templates/channels/chatgpt/install.template.md.hbs +20 -95
- package/templates/channels/chatgpt/integration/README.md +1 -1
- package/templates/channels/chatgpt/integration/application.json +2 -2
- package/templates/channels/chatgpt/integration/chatgpt-system.json +49 -4
- package/templates/channels/chatgpt/integration/env.template +2 -4
- package/templates/channels/chatgpt/integration/rbac.json +13 -0
- package/templates/channels/claude/TROUBLESHOOTING.md +41 -0
- package/templates/channels/claude/channel.json +59 -0
- package/templates/channels/claude/icon.svg +1 -0
- package/templates/channels/claude/install.template.md.hbs +21 -0
- package/templates/channels/claude/integration/README.md +14 -0
- package/templates/channels/claude/integration/application.json +15 -0
- package/templates/channels/claude/integration/claude-system.json +59 -0
- package/templates/channels/claude/integration/env.template +2 -0
- package/templates/channels/claude/integration/rbac.json +13 -0
- package/templates/channels/claude/manifest.template.json.hbs +40 -0
- package/templates/channels/mcp-client/channel.json +72 -0
- package/templates/channels/mcp-client/icon.svg +1 -0
- package/templates/channels/mcp-client/install.template.md.hbs +24 -0
- package/templates/channels/mcp-client/integration/README.md +13 -0
- package/templates/channels/mcp-client/integration/application.json +15 -0
- package/templates/channels/mcp-client/integration/env.template +2 -0
- package/templates/channels/mcp-client/integration/mcp-client-system.json +72 -0
- package/templates/channels/mcp-client/integration/rbac.json +13 -0
- package/templates/channels/mcp-client/manifest.template.json.hbs +41 -0
- package/templates/channels/mcp-client/profiles/cursor.json +26 -0
- package/templates/channels/mcp-client/profiles/github-copilot.json +29 -0
- package/templates/channels/mcp-client/profiles/vscode.json +29 -0
- package/templates/channels/microsoft-copilot/channel.json +73 -0
- package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +8 -4
- package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +65 -5
- package/templates/channels/microsoft-copilot/icon.svg +1 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +9 -9
- package/templates/channels/microsoft-copilot/integration/README.md +1 -1
- package/templates/channels/microsoft-copilot/integration/application.json +2 -2
- package/templates/channels/microsoft-copilot/integration/env.template +3 -6
- package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +63 -4
- package/templates/channels/microsoft-copilot/integration/rbac.json +13 -0
- package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +5 -5
- package/templates/channels/slack/channel.json +223 -0
- package/templates/channels/slack/icon.svg +1 -0
- package/templates/channels/slack/install.template.md.hbs +36 -28
- package/templates/channels/slack/integration/README.md +245 -22
- package/templates/channels/slack/integration/application.json +44 -7
- package/templates/channels/slack/integration/env.template +15 -6
- package/templates/channels/slack/integration/openapi/openapi.json +126 -0
- package/templates/channels/slack/integration/rbac.json +84 -0
- package/templates/channels/slack/integration/slack-datasource-commands.json +154 -0
- package/templates/channels/slack/integration/slack-datasource-conversations.json +28 -3
- package/templates/channels/slack/integration/slack-datasource-events.json +154 -0
- package/templates/channels/slack/integration/slack-datasource-interactive.json +154 -0
- package/templates/channels/slack/integration/slack-datasource-messages.json +40 -2
- package/templates/channels/slack/integration/slack-system.json +155 -77
- package/templates/channels/slack/manifest.template.json.hbs +5 -2
- package/templates/channels/teams-bot/channel.json +161 -0
- package/templates/channels/teams-bot/icon.svg +1 -0
- package/templates/channels/teams-bot/install.template.md.hbs +12 -13
- package/templates/channels/teams-bot/integration/README.md +5 -4
- package/templates/channels/teams-bot/integration/application.json +6 -4
- package/templates/channels/teams-bot/integration/env.template +5 -4
- package/templates/channels/teams-bot/integration/openapi/openapi.json +68 -0
- package/templates/channels/teams-bot/integration/rbac.json +21 -0
- package/templates/channels/teams-bot/integration/teams-bot-datasource-activities.json +154 -0
- package/templates/channels/teams-bot/integration/teams-bot-system.json +128 -23
- package/templates/channels/web-embed/channel.json +60 -0
- package/templates/channels/web-embed/install.template.md.hbs +1 -1
- package/templates/channels/web-embed/integration/README.md +2 -2
- package/templates/channels/web-embed/integration/application.json +2 -2
- package/templates/channels/web-embed/integration/env.template +1 -1
- package/templates/channels/web-embed/integration/rbac.json +13 -0
- package/templates/channels/web-embed/integration/web-embed-system.json +50 -4
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural merge / remove for editor MCP config files.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Preserve unrelated servers; never log secrets
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} filePath
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function backupPath(filePath) {
|
|
17
|
+
return `${filePath}.bak`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} filePath
|
|
22
|
+
* @param {string} contents
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
function writeBackup(filePath, contents) {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(backupPath(filePath), contents, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} filePath
|
|
32
|
+
* @param {Object} [deps]
|
|
33
|
+
* @returns {{ config: Object, recovered: boolean, existed: boolean, raw: string }}
|
|
34
|
+
*/
|
|
35
|
+
function readMcpConfig(filePath, deps = {}) {
|
|
36
|
+
const readFile = deps.readFileSync || ((p) => fs.readFileSync(p, 'utf8'));
|
|
37
|
+
const exists = deps.existsSync || ((p) => fs.existsSync(p));
|
|
38
|
+
if (!exists(filePath)) {
|
|
39
|
+
return { config: {}, recovered: false, existed: false, raw: '' };
|
|
40
|
+
}
|
|
41
|
+
const raw = readFile(filePath);
|
|
42
|
+
if (!raw || !String(raw).trim()) {
|
|
43
|
+
return { config: {}, recovered: false, existed: true, raw };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
48
|
+
return { config: parsed, recovered: false, existed: true, raw };
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
writeBackup(`${filePath}.invalid`, raw);
|
|
52
|
+
}
|
|
53
|
+
return { config: {}, recovered: true, existed: true, raw };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} config
|
|
58
|
+
* @param {Object} profile
|
|
59
|
+
* @param {Object} entry
|
|
60
|
+
* @returns {Object}
|
|
61
|
+
*/
|
|
62
|
+
function mergeServerEntry(config, profile, entry) {
|
|
63
|
+
const serversKey = profile.config.serversKey;
|
|
64
|
+
const serverKey = profile.config.serverKey;
|
|
65
|
+
const next = { ...config };
|
|
66
|
+
const servers = { ...(next[serversKey] && typeof next[serversKey] === 'object' ? next[serversKey] : {}) };
|
|
67
|
+
const previous = servers[serverKey] && typeof servers[serverKey] === 'object' ? servers[serverKey] : {};
|
|
68
|
+
servers[serverKey] = { ...previous, ...entry };
|
|
69
|
+
next[serversKey] = servers;
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {Object} config
|
|
75
|
+
* @param {Object} profile
|
|
76
|
+
* @returns {Object}
|
|
77
|
+
*/
|
|
78
|
+
function removeServerEntry(config, profile) {
|
|
79
|
+
const serversKey = profile.config.serversKey;
|
|
80
|
+
const serverKey = profile.config.serverKey;
|
|
81
|
+
const next = { ...config };
|
|
82
|
+
const servers = { ...(next[serversKey] && typeof next[serversKey] === 'object' ? next[serversKey] : {}) };
|
|
83
|
+
delete servers[serverKey];
|
|
84
|
+
if (Object.keys(servers).length === 0) {
|
|
85
|
+
delete next[serversKey];
|
|
86
|
+
} else {
|
|
87
|
+
next[serversKey] = servers;
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} filePath
|
|
94
|
+
* @param {Object} config
|
|
95
|
+
* @param {Object} [deps]
|
|
96
|
+
* @returns {void}
|
|
97
|
+
*/
|
|
98
|
+
function writeMcpConfig(filePath, config, deps = {}) {
|
|
99
|
+
const writeFile = deps.writeFileSync || ((p, contents) => fs.writeFileSync(p, contents, 'utf8'));
|
|
100
|
+
const mkdir = deps.mkdirSync || ((p, opts) => fs.mkdirSync(p, opts));
|
|
101
|
+
mkdir(path.dirname(filePath), { recursive: true });
|
|
102
|
+
writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {Object} params
|
|
107
|
+
* @returns {{ filePath: string, config: Object, recovered: boolean }}
|
|
108
|
+
*/
|
|
109
|
+
function mergeMcpClientConfig(params) {
|
|
110
|
+
const { profile, filePath, entry, deps = {} } = params;
|
|
111
|
+
const read = readMcpConfig(filePath, deps);
|
|
112
|
+
if (read.existed && read.raw) {
|
|
113
|
+
writeBackup(filePath, read.raw);
|
|
114
|
+
}
|
|
115
|
+
const config = mergeServerEntry(read.config, profile, entry);
|
|
116
|
+
writeMcpConfig(filePath, config, deps);
|
|
117
|
+
return { filePath, config, recovered: read.recovered };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove only the AI Fabrix server key.
|
|
122
|
+
* @param {Object} params
|
|
123
|
+
* @returns {{ filePath: string, config: Object, removed: boolean }}
|
|
124
|
+
*/
|
|
125
|
+
function removeMcpClientConfig(params) {
|
|
126
|
+
const { profile, filePath, deps = {} } = params;
|
|
127
|
+
const read = readMcpConfig(filePath, deps);
|
|
128
|
+
const servers = read.config[profile.config.serversKey] || {};
|
|
129
|
+
const removed = Boolean(servers[profile.config.serverKey]);
|
|
130
|
+
if (!removed) {
|
|
131
|
+
return { filePath, config: read.config, removed: false };
|
|
132
|
+
}
|
|
133
|
+
if (read.existed && read.raw) {
|
|
134
|
+
writeBackup(filePath, read.raw);
|
|
135
|
+
}
|
|
136
|
+
const config = removeServerEntry(read.config, profile);
|
|
137
|
+
writeMcpConfig(filePath, config, deps);
|
|
138
|
+
return { filePath, config, removed: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
backupPath,
|
|
143
|
+
readMcpConfig,
|
|
144
|
+
mergeServerEntry,
|
|
145
|
+
removeServerEntry,
|
|
146
|
+
mergeMcpClientConfig,
|
|
147
|
+
removeMcpClientConfig
|
|
148
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure the per-installation Integration Client for an MCP client profile.
|
|
3
|
+
*
|
|
4
|
+
* Create when missing; merge redirect URIs; never rotate the secret.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Reuse existing Integration Client APIs only
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
createIntegrationClient,
|
|
13
|
+
listIntegrationClients,
|
|
14
|
+
getIntegrationClient,
|
|
15
|
+
updateIntegrationClientRedirectUris
|
|
16
|
+
} = require('../../api/integration-clients.api');
|
|
17
|
+
const {
|
|
18
|
+
unwrapIntegrationClientList
|
|
19
|
+
} = require('../chatgpt-callback');
|
|
20
|
+
const {
|
|
21
|
+
normalizeMcpClientRedirectUris,
|
|
22
|
+
mergeRedirectUriList
|
|
23
|
+
} = require('./redirect-uris');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @param {string} [boundary]
|
|
28
|
+
* @returns {Error}
|
|
29
|
+
*/
|
|
30
|
+
function createMcpClientEnsureError(message, boundary) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.boundary = boundary || 'oauth';
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object} response
|
|
38
|
+
* @returns {Object}
|
|
39
|
+
*/
|
|
40
|
+
function unwrapRecord(response) {
|
|
41
|
+
const body = response && response.data !== undefined ? response.data : response || {};
|
|
42
|
+
const inner = body.data !== undefined && body.data !== null ? body.data : body;
|
|
43
|
+
return inner.integrationClient || inner;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Error|Object} errorOrResponse
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
function isForbidden(errorOrResponse) {
|
|
51
|
+
const status = errorOrResponse && errorOrResponse.status;
|
|
52
|
+
if (status === 403) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const message = errorOrResponse && errorOrResponse.message
|
|
56
|
+
? String(errorOrResponse.message)
|
|
57
|
+
: '';
|
|
58
|
+
return /\b403\b/.test(message) || /forbidden|not allowed|integration-client:/i.test(message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Error|Object} errorOrResponse
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isLoopbackRejected(errorOrResponse) {
|
|
66
|
+
const status = errorOrResponse && errorOrResponse.status;
|
|
67
|
+
const message = errorOrResponse && errorOrResponse.message
|
|
68
|
+
? String(errorOrResponse.message)
|
|
69
|
+
: String(errorOrResponse || '');
|
|
70
|
+
if (status !== 400 && !/\b400\b/.test(message)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return /localhost|127\.0\.0\.1|loopback|http:\/\//i.test(message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {Object} ctx
|
|
78
|
+
* @param {string} key
|
|
79
|
+
* @param {Object} deps
|
|
80
|
+
* @returns {Promise<Object|null>}
|
|
81
|
+
*/
|
|
82
|
+
async function findClientByKey(ctx, key, deps) {
|
|
83
|
+
const listFn = deps.listIntegrationClients || listIntegrationClients;
|
|
84
|
+
const response = await listFn(ctx.controllerUrl, ctx.authConfig, { pageSize: 100 });
|
|
85
|
+
if (response && response.success === false) {
|
|
86
|
+
if (response.status === 403 || isForbidden(response)) {
|
|
87
|
+
throw createMcpClientEnsureError(
|
|
88
|
+
'Missing integration-client:read. Cannot list Integration Clients.',
|
|
89
|
+
'permissions'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw createMcpClientEnsureError(
|
|
93
|
+
response.formattedError || response.error || 'Failed to list integration clients.'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return unwrapIntegrationClientList(response).find((row) => row && row.key === key) || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {Object} ctx
|
|
101
|
+
* @param {Object} match
|
|
102
|
+
* @param {Object} deps
|
|
103
|
+
* @returns {Promise<Object>}
|
|
104
|
+
*/
|
|
105
|
+
async function loadClientDetail(ctx, match, deps) {
|
|
106
|
+
const getFn = deps.getIntegrationClient || getIntegrationClient;
|
|
107
|
+
const detail = await getFn(ctx.controllerUrl, ctx.authConfig, match.id);
|
|
108
|
+
const record = unwrapRecord(detail);
|
|
109
|
+
return { ...match, ...record, id: match.id };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {Object} params
|
|
114
|
+
* @returns {Object}
|
|
115
|
+
*/
|
|
116
|
+
function createClientBody(params) {
|
|
117
|
+
const { profile, redirectUris } = params;
|
|
118
|
+
return {
|
|
119
|
+
key: profile.oauth.integrationClientKey,
|
|
120
|
+
displayName: profile.displayName,
|
|
121
|
+
redirectUris,
|
|
122
|
+
keycloakClientId: profile.oauth.keycloakClientId,
|
|
123
|
+
description: `Developer MCP client (${profile.key})`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {Object} response
|
|
129
|
+
* @returns {string|undefined}
|
|
130
|
+
*/
|
|
131
|
+
function oneTimeSecret(response) {
|
|
132
|
+
const record = unwrapRecord(response);
|
|
133
|
+
const secret = record && record.clientSecret;
|
|
134
|
+
return typeof secret === 'string' && secret.trim() ? secret : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {Object} ctx
|
|
139
|
+
* @param {Object} profile
|
|
140
|
+
* @param {string[]} redirectUris
|
|
141
|
+
* @param {Object} deps
|
|
142
|
+
* @returns {Promise<{ client: Object, created: boolean, clientSecret?: string }>}
|
|
143
|
+
*/
|
|
144
|
+
async function createClient(ctx, profile, redirectUris, deps) {
|
|
145
|
+
const createFn = deps.createIntegrationClient || createIntegrationClient;
|
|
146
|
+
try {
|
|
147
|
+
const response = await createFn(ctx.controllerUrl, ctx.authConfig, createClientBody({
|
|
148
|
+
profile,
|
|
149
|
+
redirectUris
|
|
150
|
+
}));
|
|
151
|
+
if (response && response.success === false) {
|
|
152
|
+
throw Object.assign(new Error(response.formattedError || response.error || 'Create failed'), {
|
|
153
|
+
status: response.status
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const record = unwrapRecord(response);
|
|
157
|
+
return { client: record, created: true, clientSecret: oneTimeSecret(response) };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isForbidden(error)) {
|
|
160
|
+
throw createMcpClientEnsureError(
|
|
161
|
+
'Missing integration-client:create. Cannot provision the MCP client.',
|
|
162
|
+
'permissions'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (isLoopbackRejected(error)) {
|
|
166
|
+
throw createMcpClientEnsureError(
|
|
167
|
+
'Keycloak rejected a loopback HTTP redirect URI. Do not fall back to API keys.',
|
|
168
|
+
'oauth'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {Object} params
|
|
177
|
+
* @returns {Promise<{ ok: boolean, added: boolean }>}
|
|
178
|
+
*/
|
|
179
|
+
async function mergeRedirects(params) {
|
|
180
|
+
const { ctx, client, redirectUris, deps } = params;
|
|
181
|
+
const updateFn = deps.updateIntegrationClientRedirectUris || updateIntegrationClientRedirectUris;
|
|
182
|
+
const merged = mergeRedirectUriList(client.redirectUris, redirectUris);
|
|
183
|
+
try {
|
|
184
|
+
const response = await updateFn(ctx.controllerUrl, ctx.authConfig, client.id, {
|
|
185
|
+
redirectUris: merged.uris
|
|
186
|
+
});
|
|
187
|
+
if (response && response.success === false) {
|
|
188
|
+
throw Object.assign(new Error(response.formattedError || response.error || 'Update failed'), {
|
|
189
|
+
status: response.status
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (isForbidden(error)) {
|
|
194
|
+
throw createMcpClientEnsureError(
|
|
195
|
+
'Missing integration-client:update. Cannot merge redirect URIs.',
|
|
196
|
+
'permissions'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (isLoopbackRejected(error)) {
|
|
200
|
+
throw createMcpClientEnsureError(
|
|
201
|
+
'Keycloak rejected a loopback HTTP redirect URI. Do not fall back to API keys.',
|
|
202
|
+
'oauth'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
return { ok: true, added: merged.added, redirectUris: merged.uris };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create or reuse the Integration Client. Never calls regenerate-secret.
|
|
212
|
+
* @param {Object} params
|
|
213
|
+
* @returns {Promise<{ client: Object, created: boolean, added: boolean, clientSecret?: string }>}
|
|
214
|
+
*/
|
|
215
|
+
async function ensureMcpClientIntegrationClient(params) {
|
|
216
|
+
const { ctx, profile, deps = {} } = params;
|
|
217
|
+
const redirectUris = normalizeMcpClientRedirectUris(profile.oauth.redirectUris);
|
|
218
|
+
const match = await findClientByKey(ctx, profile.oauth.integrationClientKey, deps);
|
|
219
|
+
if (!match || !match.id) {
|
|
220
|
+
const created = await createClient(ctx, profile, redirectUris, deps);
|
|
221
|
+
return { ...created, added: true };
|
|
222
|
+
}
|
|
223
|
+
const client = await loadClientDetail(ctx, match, deps);
|
|
224
|
+
const merged = await mergeRedirects({ ctx, client, redirectUris, deps });
|
|
225
|
+
return { client: { ...client, redirectUris: merged.redirectUris }, created: false, added: merged.added };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
ensureMcpClientIntegrationClient,
|
|
230
|
+
createMcpClientEnsureError
|
|
231
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve editor MCP config paths for installation profiles.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview User vs project mcp.json locations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} [deps]
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function vscodeUserMcpPath(deps = {}) {
|
|
17
|
+
const homedir = (deps.homedir || os.homedir)();
|
|
18
|
+
const env = deps.env || process.env;
|
|
19
|
+
const platform = deps.platform || process.platform;
|
|
20
|
+
if (platform === 'darwin') {
|
|
21
|
+
return path.join(homedir, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
22
|
+
}
|
|
23
|
+
if (platform === 'win32') {
|
|
24
|
+
const appData = env.APPDATA || path.join(homedir, 'AppData', 'Roaming');
|
|
25
|
+
return path.join(appData, 'Code', 'User', 'mcp.json');
|
|
26
|
+
}
|
|
27
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(homedir, '.config');
|
|
28
|
+
return path.join(xdg, 'Code', 'User', 'mcp.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {Object} profile
|
|
33
|
+
* @param {Object} [deps]
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function resolveMcpConfigPath(profile, deps = {}) {
|
|
37
|
+
const homedir = deps.homedir || os.homedir;
|
|
38
|
+
const cwd = deps.cwd || process.cwd;
|
|
39
|
+
const relative = profile.config.relativePath;
|
|
40
|
+
if (profile.config.scope === 'project') {
|
|
41
|
+
return path.join(cwd(), relative);
|
|
42
|
+
}
|
|
43
|
+
const key = profile.key;
|
|
44
|
+
if (key === 'vscode' || key === 'github-copilot') {
|
|
45
|
+
return vscodeUserMcpPath(deps);
|
|
46
|
+
}
|
|
47
|
+
return path.join(homedir(), relative);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
vscodeUserMcpPath,
|
|
52
|
+
resolveMcpConfigPath
|
|
53
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP client installation profile loader (schema-validated).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Load cursor / vscode / github-copilot profiles from channel templates
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const Ajv = require('ajv');
|
|
12
|
+
const schema = require('../../schema/mcp-client-profile.schema.json');
|
|
13
|
+
|
|
14
|
+
const BUILDER_ROOT = path.join(__dirname, '..', '..', '..');
|
|
15
|
+
const PROFILE_DIR = path.join(BUILDER_ROOT, 'templates', 'channels', 'mcp-client', 'profiles');
|
|
16
|
+
|
|
17
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
18
|
+
const validateProfile = ajv.compile(schema);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} message
|
|
22
|
+
* @returns {Error}
|
|
23
|
+
*/
|
|
24
|
+
function createMcpClientProfileError(message) {
|
|
25
|
+
const error = new Error(message);
|
|
26
|
+
error.boundary = 'usage';
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
33
|
+
function listMcpClientProfileKeys() {
|
|
34
|
+
return fs
|
|
35
|
+
.readdirSync(PROFILE_DIR)
|
|
36
|
+
.filter((name) => name.endsWith('.json'))
|
|
37
|
+
.map((name) => path.basename(name, '.json'))
|
|
38
|
+
.sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @returns {{ name: string, value: string }[]}
|
|
43
|
+
*/
|
|
44
|
+
function listMcpClientProfileChoices() {
|
|
45
|
+
return listMcpClientProfileKeys().map((key) => {
|
|
46
|
+
const profile = loadMcpClientProfile(key);
|
|
47
|
+
return { name: profile.displayName, value: profile.key };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Object} profile
|
|
53
|
+
* @param {string} key
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
function assertValidProfile(profile, key) {
|
|
57
|
+
if (validateProfile(profile)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const details = (validateProfile.errors || [])
|
|
61
|
+
.map((err) => `${err.instancePath || '/'} ${err.message}`)
|
|
62
|
+
.join('; ');
|
|
63
|
+
throw createMcpClientProfileError(`MCP client profile "${key}" is invalid: ${details}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} key
|
|
68
|
+
* @returns {Object}
|
|
69
|
+
*/
|
|
70
|
+
function loadMcpClientProfile(key) {
|
|
71
|
+
const trimmed = typeof key === 'string' ? key.trim() : '';
|
|
72
|
+
if (!trimmed) {
|
|
73
|
+
throw createMcpClientProfileError(
|
|
74
|
+
[
|
|
75
|
+
'MCP client needs --target <profile> (cursor, vscode, or github-copilot).',
|
|
76
|
+
' $ aifabrix channel add mcp-client --target cursor --name "Onni"'
|
|
77
|
+
].join('\n')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const filePath = path.join(PROFILE_DIR, `${trimmed}.json`);
|
|
81
|
+
if (!fs.existsSync(filePath)) {
|
|
82
|
+
const known = listMcpClientProfileKeys().join(', ');
|
|
83
|
+
throw createMcpClientProfileError(
|
|
84
|
+
`Unknown MCP client profile: ${trimmed}\nSupported profiles: ${known}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const profile = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
88
|
+
assertValidProfile(profile, trimmed);
|
|
89
|
+
if (profile.key !== trimmed) {
|
|
90
|
+
throw createMcpClientProfileError(
|
|
91
|
+
`MCP client profile "${trimmed}" key mismatch (found "${profile.key}").`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return profile;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {Object} [options]
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function resolveMcpClientProfileKey(options = {}) {
|
|
102
|
+
const raw = options.target || options.profile || options.mcpClientProfile;
|
|
103
|
+
return raw && String(raw).trim() ? String(raw).trim() : '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Object} [options]
|
|
108
|
+
* @returns {Object}
|
|
109
|
+
*/
|
|
110
|
+
function requireMcpClientProfile(options = {}) {
|
|
111
|
+
return loadMcpClientProfile(resolveMcpClientProfileKey(options));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
PROFILE_DIR,
|
|
116
|
+
listMcpClientProfileKeys,
|
|
117
|
+
listMcpClientProfileChoices,
|
|
118
|
+
loadMcpClientProfile,
|
|
119
|
+
resolveMcpClientProfileKey,
|
|
120
|
+
requireMcpClientProfile,
|
|
121
|
+
assertValidProfile
|
|
122
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP client redirect URI validation (HTTPS plus loopback HTTP).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Do not reuse ChatGPT HTTPS-only callback assert
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { mergeRedirectUris } = require('../chatgpt-callback');
|
|
10
|
+
|
|
11
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @returns {Error}
|
|
16
|
+
*/
|
|
17
|
+
function createMcpClientRedirectError(message) {
|
|
18
|
+
const error = new Error(`MCP client callback: ${message}`);
|
|
19
|
+
error.boundary = 'callback';
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} hostname
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function isLoopbackHost(hostname) {
|
|
28
|
+
const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
29
|
+
return LOOPBACK_HOSTS.has(host) || LOOPBACK_HOSTS.has(hostname);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Allow HTTPS anywhere and HTTP on loopback only.
|
|
34
|
+
* @param {string|undefined} value
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function assertMcpClientRedirectUri(value) {
|
|
38
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
throw createMcpClientRedirectError('Redirect URI is required.');
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = new URL(trimmed);
|
|
45
|
+
} catch {
|
|
46
|
+
throw createMcpClientRedirectError(`Redirect URI is not a valid URL: ${trimmed}`);
|
|
47
|
+
}
|
|
48
|
+
if (parsed.protocol === 'https:') {
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
if (parsed.protocol === 'http:' && isLoopbackHost(parsed.hostname)) {
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
throw createMcpClientRedirectError(
|
|
55
|
+
`Redirect URI must be HTTPS or loopback HTTP: ${trimmed}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string[]|undefined} values
|
|
61
|
+
* @returns {string[]}
|
|
62
|
+
*/
|
|
63
|
+
function normalizeMcpClientRedirectUris(values) {
|
|
64
|
+
const unique = [];
|
|
65
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
66
|
+
const uri = assertMcpClientRedirectUri(value);
|
|
67
|
+
if (!unique.includes(uri)) {
|
|
68
|
+
unique.push(uri);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (unique.length === 0) {
|
|
72
|
+
throw createMcpClientRedirectError('At least one redirect URI is required.');
|
|
73
|
+
}
|
|
74
|
+
return unique;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string[]|undefined} existing
|
|
79
|
+
* @param {string[]} nextUris
|
|
80
|
+
* @returns {{ uris: string[], added: boolean }}
|
|
81
|
+
*/
|
|
82
|
+
function mergeRedirectUriList(existing, nextUris) {
|
|
83
|
+
let uris = Array.isArray(existing) ? [...existing] : [];
|
|
84
|
+
let added = false;
|
|
85
|
+
for (const nextUri of nextUris) {
|
|
86
|
+
const merged = mergeRedirectUris(uris, nextUri);
|
|
87
|
+
uris = merged.uris;
|
|
88
|
+
added = added || merged.added;
|
|
89
|
+
}
|
|
90
|
+
return { uris, added };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
LOOPBACK_HOSTS,
|
|
95
|
+
assertMcpClientRedirectUri,
|
|
96
|
+
normalizeMcpClientRedirectUris,
|
|
97
|
+
mergeRedirectUris,
|
|
98
|
+
mergeRedirectUriList
|
|
99
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime values for the generic developer MCP client channel.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Same Runtime MCP as ChatGPT; OAuth profile from the installation profile
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
CHATGPT_RUNTIME_SYSTEM_KEY,
|
|
11
|
+
CHATGPT_RUNTIME_TRANSPORT,
|
|
12
|
+
CHATGPT_RUNTIME_AUTH_MODE
|
|
13
|
+
} = require('../chatgpt-runtime-contract');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} controllerUrl
|
|
17
|
+
* @param {string} dataplaneBaseUrl
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function authorizationServerBase(controllerUrl, dataplaneBaseUrl) {
|
|
21
|
+
const candidate = controllerUrl || dataplaneBaseUrl;
|
|
22
|
+
try {
|
|
23
|
+
return `${new URL(candidate).origin}/auth/realms/aifabrix`;
|
|
24
|
+
} catch {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Object} resolved
|
|
31
|
+
* @param {Object} params
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function applyMcpClientRuntimeValues(resolved, params) {
|
|
35
|
+
resolved.runtimeSystemKey = CHATGPT_RUNTIME_SYSTEM_KEY;
|
|
36
|
+
resolved.runtimeTransport = CHATGPT_RUNTIME_TRANSPORT;
|
|
37
|
+
resolved.runtimeAuthMode = CHATGPT_RUNTIME_AUTH_MODE;
|
|
38
|
+
resolved.layerBToolNames = Array.isArray(params.layerBToolNames) ? [...params.layerBToolNames] : [];
|
|
39
|
+
resolved.scopes = Array.isArray(params.scopes) ? [...params.scopes] : [];
|
|
40
|
+
resolved.scopesJson = JSON.stringify(resolved.scopes);
|
|
41
|
+
const profile = params.mcpClientProfile;
|
|
42
|
+
if (!profile) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const clientId = profile.oauth.keycloakClientId;
|
|
46
|
+
const controllerUrl = typeof params.controllerUrl === 'string' ? params.controllerUrl.trim() : '';
|
|
47
|
+
resolved.mcpClientProfileKey = profile.key;
|
|
48
|
+
resolved.mcpClientDisplayName = profile.displayName;
|
|
49
|
+
resolved.oauth2ProfileId = clientId;
|
|
50
|
+
resolved.oauth2ProfilePath = `/external-access/oauth2-profiles/${clientId}`;
|
|
51
|
+
if (controllerUrl) {
|
|
52
|
+
resolved.oauth2ProfileUrl = `${controllerUrl.replace(/\/$/, '')}${resolved.oauth2ProfilePath}`;
|
|
53
|
+
}
|
|
54
|
+
resolved.authorizationServerBase = authorizationServerBase(controllerUrl, resolved.dataplaneBaseUrl);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
applyMcpClientRuntimeValues
|
|
59
|
+
};
|