@documentation-robotics/cli 0.1.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +171 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +112 -28
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +3 -1
- package/dist/coding-agents/agents/claude-code.js.map +2 -2
- package/dist/coding-agents/claude-code-client.js +2 -1
- package/dist/coding-agents/claude-code-client.js.map +2 -2
- package/dist/coding-agents/copilot-client.js +17 -12
- package/dist/coding-agents/copilot-client.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +152 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/coding-agents/drbot-mcp.js +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +12 -7
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +134 -23
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/audit.js +6 -6
- package/dist/commands/audit.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +261 -147
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +52 -19
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/element.js +3 -2
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/farm.js +773 -0
- package/dist/commands/farm.js.map +7 -0
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +122 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +251 -47
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +56 -24
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +41 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +2 -2
- package/dist/commands/version.js.map +2 -2
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/config.js +83 -0
- package/dist/config.js.map +7 -0
- package/dist/core/annotation-store.js +138 -0
- package/dist/core/annotation-store.js.map +7 -0
- package/dist/core/catalog-migration.js +33 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +153 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +518 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/farm-manifest.js +140 -0
- package/dist/core/farm-manifest.js.map +7 -0
- package/dist/core/farm-sync-engine.js +346 -0
- package/dist/core/farm-sync-engine.js.map +7 -0
- package/dist/core/farm-sync-state.js +167 -0
- package/dist/core/farm-sync-state.js.map +7 -0
- package/dist/core/manifest.js +6 -3
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +84 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +257 -44
- package/dist/core/model.js.map +3 -3
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +68 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +64 -17
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +83 -18
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/virtual-projection.js +14 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2209 -7727
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +79 -0
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +52 -1
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +111 -0
- package/dist/mcp/api-key-manager.js.map +7 -0
- package/dist/mcp/changeset-only.js +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/http-transport.js +176 -0
- package/dist/mcp/http-transport.js.map +7 -0
- package/dist/mcp/resource-registry.js +97 -0
- package/dist/mcp/resource-registry.js.map +7 -0
- package/dist/mcp/tool-registry.js +60 -0
- package/dist/mcp/tool-registry.js.map +7 -0
- package/dist/mcp/tools/annotation-create.js +39 -0
- package/dist/mcp/tools/annotation-create.js.map +7 -0
- package/dist/mcp/tools/annotation-delete.js +29 -0
- package/dist/mcp/tools/annotation-delete.js.map +7 -0
- package/dist/mcp/tools/annotation-get.js +29 -0
- package/dist/mcp/tools/annotation-get.js.map +7 -0
- package/dist/mcp/tools/annotation-list.js +24 -0
- package/dist/mcp/tools/annotation-list.js.map +7 -0
- package/dist/mcp/tools/annotation-reply.js +34 -0
- package/dist/mcp/tools/annotation-reply.js.map +7 -0
- package/dist/mcp/tools/annotation-shared.js +10 -0
- package/dist/mcp/tools/annotation-shared.js.map +7 -0
- package/dist/mcp/tools/annotation-update.js +39 -0
- package/dist/mcp/tools/annotation-update.js.map +7 -0
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-list.js +33 -0
- package/dist/mcp/tools/changeset-list.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +55 -0
- package/dist/mcp/tools/changeset-show.js.map +7 -0
- package/dist/mcp/tools/changeset-verify.js +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/chat-launch.js +50 -0
- package/dist/mcp/tools/chat-launch.js.map +7 -0
- package/dist/mcp/tools/chat-status.js +26 -0
- package/dist/mcp/tools/chat-status.js.map +7 -0
- package/dist/mcp/tools/index.js +65 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +107 -0
- package/dist/mcp/tools/model-add.js.map +7 -0
- package/dist/mcp/tools/model-delete.js +99 -0
- package/dist/mcp/tools/model-delete.js.map +7 -0
- package/dist/mcp/tools/model-export.js +72 -0
- package/dist/mcp/tools/model-export.js.map +7 -0
- package/dist/mcp/tools/model-info.js +51 -0
- package/dist/mcp/tools/model-info.js.map +7 -0
- package/dist/mcp/tools/model-list.js +53 -0
- package/dist/mcp/tools/model-list.js.map +7 -0
- package/dist/mcp/tools/model-reload.js +34 -0
- package/dist/mcp/tools/model-reload.js.map +7 -0
- package/dist/mcp/tools/model-search.js +102 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +68 -0
- package/dist/mcp/tools/model-show.js.map +7 -0
- package/dist/mcp/tools/model-stats.js +22 -0
- package/dist/mcp/tools/model-stats.js.map +7 -0
- package/dist/mcp/tools/model-trace.js +118 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +121 -0
- package/dist/mcp/tools/model-update.js.map +7 -0
- package/dist/mcp/tools/model-validate.js +78 -0
- package/dist/mcp/tools/model-validate.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +126 -0
- package/dist/mcp/tools/shared.js.map +7 -0
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +5 -3
- package/dist/reports/model-layer-report-generator.js.map +2 -2
- package/dist/reports/model-readme-generator.js +116 -0
- package/dist/reports/model-readme-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +37 -1
- package/dist/reports/model-report-data.js.map +2 -2
- package/dist/reports/model-report-orchestrator.js +29 -6
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/reports/model-report-utils.js +9 -0
- package/dist/reports/model-report-utils.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +745 -370
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +403 -383
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +718 -1643
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +234 -31
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/config.js +2 -40
- package/dist/telemetry/config.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/types/index.js.map +2 -2
- package/dist/utils/ansi-suppressor.js +23 -0
- package/dist/utils/ansi-suppressor.js.map +7 -0
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/color-utils.js +34 -0
- package/dist/utils/color-utils.js.map +7 -0
- package/dist/utils/element-reference-shape.js +15 -1
- package/dist/utils/element-reference-shape.js.map +3 -3
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +75 -10
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/globals.js +17 -1
- package/dist/utils/globals.js.map +2 -2
- package/dist/utils/project-paths.js +50 -3
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/reference-path-parser.js +49 -0
- package/dist/utils/reference-path-parser.js.map +7 -0
- package/dist/utils/source-reference.js +17 -10
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +10 -2
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/validators/composed-reference-validator.js +240 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +57 -13
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +33 -5
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +7 -3
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { DRConfigParseError, loadDRConfig, saveDRConfig } from "../config.js";
|
|
7
|
+
const KEY_FILE_MODE = 384;
|
|
8
|
+
function getDefaultApiKeyPath() {
|
|
9
|
+
return join(homedir(), ".dr-mcp-key");
|
|
10
|
+
}
|
|
11
|
+
class ApiKeyManager {
|
|
12
|
+
defaultKeyPath;
|
|
13
|
+
/**
|
|
14
|
+
* @param defaultKeyPath Path offered as the default storage location when no
|
|
15
|
+
* key is yet configured. Defaults to `getDefaultApiKeyPath()`; overridable
|
|
16
|
+
* for tests so they never touch the real home directory.
|
|
17
|
+
*/
|
|
18
|
+
constructor(defaultKeyPath = getDefaultApiKeyPath()) {
|
|
19
|
+
this.defaultKeyPath = defaultKeyPath;
|
|
20
|
+
}
|
|
21
|
+
/** Generate a new random API key. */
|
|
22
|
+
generate() {
|
|
23
|
+
return `dr-mcp-${randomBytes(32).toString("base64url")}`;
|
|
24
|
+
}
|
|
25
|
+
/** Read a previously stored key from disk. */
|
|
26
|
+
async load(path) {
|
|
27
|
+
const key = (await readFile(path, "utf-8")).trim();
|
|
28
|
+
if (!key)
|
|
29
|
+
throw new Error(`API key file at ${path} is empty or corrupt. Delete it and re-run to generate a new key, or restore a valid key file.`);
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
/** The key stored at `path`, or null when there is no usable key file there. */
|
|
33
|
+
async loadIfPresent(path) {
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return await this.load(path);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Write a key to disk with owner-only permissions. */
|
|
43
|
+
async store(path, key) {
|
|
44
|
+
await mkdir(dirname(path), { recursive: !0 }), await writeFile(path, `${key}
|
|
45
|
+
`, { mode: KEY_FILE_MODE }), await chmod(path, KEY_FILE_MODE);
|
|
46
|
+
}
|
|
47
|
+
/** Constant-time comparison of a client-provided key against the expected key. */
|
|
48
|
+
validate(provided, expected) {
|
|
49
|
+
if (!provided)
|
|
50
|
+
return !1;
|
|
51
|
+
const providedBuf = Buffer.from(provided), expectedBuf = Buffer.from(expected);
|
|
52
|
+
return providedBuf.length !== expectedBuf.length ? !1 : timingSafeEqual(providedBuf, expectedBuf);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Load the DR config, refusing to proceed if the file exists but is
|
|
56
|
+
* corrupted. Without this, a corrupted config looks identical to "no key
|
|
57
|
+
* configured yet", which would cause `ensureKey`/`rotate` to silently
|
|
58
|
+
* generate a new key and overwrite `mcp.api_key_path` — orphaning any
|
|
59
|
+
* valid key already on disk and breaking every MCP client configured with
|
|
60
|
+
* the old one.
|
|
61
|
+
*/
|
|
62
|
+
async loadConfigOrThrow() {
|
|
63
|
+
try {
|
|
64
|
+
return await loadDRConfig({ strict: !0 });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw error instanceof DRConfigParseError ? new Error(`Cannot determine the configured MCP API key path: ${error.configPath} is corrupted. Fix or remove this file before continuing \u2014 proceeding could orphan an existing key and break already-configured MCP clients.`, { cause: error }) : error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the API key to use for this session: load it from the previously
|
|
71
|
+
* configured path, or generate+store a new one on first use.
|
|
72
|
+
*
|
|
73
|
+
* @param promptForPath Called only when no key is yet configured, to ask the
|
|
74
|
+
* user where to store it. When omitted (e.g. non-interactive launches),
|
|
75
|
+
* `defaultKeyPath` is used without prompting.
|
|
76
|
+
*/
|
|
77
|
+
async ensureKey(promptForPath) {
|
|
78
|
+
const config = await this.loadConfigOrThrow(), existingPath = config.mcp?.api_key_path;
|
|
79
|
+
if (existingPath && existsSync(existingPath))
|
|
80
|
+
return { key: await this.load(existingPath), path: existingPath, isNew: !1 };
|
|
81
|
+
const chosenPath = promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath, adopted = await this.loadIfPresent(chosenPath);
|
|
82
|
+
if (adopted)
|
|
83
|
+
return await saveDRConfig({ ...config, mcp: { ...config.mcp, api_key_path: chosenPath } }), { key: adopted, path: chosenPath, isNew: !1 };
|
|
84
|
+
const key = this.generate();
|
|
85
|
+
return await this.store(chosenPath, key), await saveDRConfig({
|
|
86
|
+
...config,
|
|
87
|
+
mcp: { ...config.mcp, api_key_path: chosenPath }
|
|
88
|
+
}), { key, path: chosenPath, isNew: !0 };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Force-generate a new API key, overwriting whatever is at the configured
|
|
92
|
+
* (or default) storage path, regardless of whether a key already exists
|
|
93
|
+
* there. Unlike `ensureKey`, this always writes a fresh key.
|
|
94
|
+
*
|
|
95
|
+
* @param promptForPath Called only when no storage path is yet configured,
|
|
96
|
+
* to ask the user where to store it. When omitted, `defaultKeyPath` is
|
|
97
|
+
* used without prompting.
|
|
98
|
+
*/
|
|
99
|
+
async rotate(promptForPath) {
|
|
100
|
+
const config = await this.loadConfigOrThrow(), path = config.mcp?.api_key_path ?? (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath), key = this.generate();
|
|
101
|
+
return await this.store(path, key), await saveDRConfig({
|
|
102
|
+
...config,
|
|
103
|
+
mcp: { ...config.mcp, api_key_path: path }
|
|
104
|
+
}), { key, path };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
ApiKeyManager,
|
|
109
|
+
getDefaultApiKeyPath
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=api-key-manager.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/api-key-manager.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * MCP API key lifecycle management\n *\n * The API key is a per-installation shared secret that authenticates MCP client\n * connections to `dr mcp` (see cli/src/commands/mcp.ts). It is generated once,\n * stored on disk at a user-chosen location, and its path is recorded under\n * `mcp.api_key_path` in ~/.dr-config.yaml so subsequent launches can find it\n * without re-prompting.\n */\n\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { DRConfig, DRConfigParseError, loadDRConfig, saveDRConfig } from \"../config.js\";\n\n/** Owner read/write only \u2014 the key file must not be group/world readable. */\nconst KEY_FILE_MODE = 0o600;\n\n/**\n * Default location for a newly generated API key. Resolved lazily (rather than\n * cached at module load) so it always reflects the current home directory \u2014\n * relevant for tests that override HOME.\n */\nexport function getDefaultApiKeyPath(): string {\n return join(homedir(), \".dr-mcp-key\");\n}\n\nexport interface EnsureKeyResult {\n key: string;\n path: string;\n /** True when a new key was generated on this call (vs. loaded from an existing path). */\n isNew: boolean;\n}\n\nexport interface RotateKeyResult {\n key: string;\n path: string;\n}\n\n/** Prompts the user for a storage path, given the default path to offer. */\nexport type ApiKeyStoragePrompt = (defaultPath: string) => Promise<string>;\n\nexport class ApiKeyManager {\n /**\n * @param defaultKeyPath Path offered as the default storage location when no\n * key is yet configured. Defaults to `getDefaultApiKeyPath()`; overridable\n * for tests so they never touch the real home directory.\n */\n constructor(private readonly defaultKeyPath: string = getDefaultApiKeyPath()) {}\n\n /** Generate a new random API key. */\n generate(): string {\n return `dr-mcp-${randomBytes(32).toString(\"base64url\")}`;\n }\n\n /** Read a previously stored key from disk. */\n async load(path: string): Promise<string> {\n const content = await readFile(path, \"utf-8\");\n const key = content.trim();\n\n if (!key) {\n throw new Error(\n `API key file at ${path} is empty or corrupt. ` +\n \"Delete it and re-run to generate a new key, or restore a valid key file.\"\n );\n }\n\n return key;\n }\n\n /** The key stored at `path`, or null when there is no usable key file there. */\n private async loadIfPresent(path: string): Promise<string | null> {\n if (!existsSync(path)) return null;\n try {\n return await this.load(path);\n } catch {\n // Empty or unreadable: ensureKey replaces it with a fresh key, as it always has.\n return null;\n }\n }\n\n /** Write a key to disk with owner-only permissions. */\n async store(path: string, key: string): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${key}\\n`, { mode: KEY_FILE_MODE });\n // writeFile's mode is only applied when creating a new file; force it\n // in case the file already existed with different permissions.\n await chmod(path, KEY_FILE_MODE);\n }\n\n /** Constant-time comparison of a client-provided key against the expected key. */\n validate(provided: string | undefined | null, expected: string): boolean {\n if (!provided) return false;\n\n const providedBuf = Buffer.from(provided);\n const expectedBuf = Buffer.from(expected);\n if (providedBuf.length !== expectedBuf.length) return false;\n\n return timingSafeEqual(providedBuf, expectedBuf);\n }\n\n /**\n * Load the DR config, refusing to proceed if the file exists but is\n * corrupted. Without this, a corrupted config looks identical to \"no key\n * configured yet\", which would cause `ensureKey`/`rotate` to silently\n * generate a new key and overwrite `mcp.api_key_path` \u2014 orphaning any\n * valid key already on disk and breaking every MCP client configured with\n * the old one.\n */\n private async loadConfigOrThrow(): Promise<DRConfig> {\n try {\n return await loadDRConfig({ strict: true });\n } catch (error) {\n if (error instanceof DRConfigParseError) {\n throw new Error(\n `Cannot determine the configured MCP API key path: ${error.configPath} is corrupted. ` +\n \"Fix or remove this file before continuing \u2014 proceeding could orphan an existing key \" +\n \"and break already-configured MCP clients.\",\n { cause: error }\n );\n }\n throw error;\n }\n }\n\n /**\n * Resolve the API key to use for this session: load it from the previously\n * configured path, or generate+store a new one on first use.\n *\n * @param promptForPath Called only when no key is yet configured, to ask the\n * user where to store it. When omitted (e.g. non-interactive launches),\n * `defaultKeyPath` is used without prompting.\n */\n async ensureKey(promptForPath?: ApiKeyStoragePrompt): Promise<EnsureKeyResult> {\n const config = await this.loadConfigOrThrow();\n const existingPath = config.mcp?.api_key_path;\n\n if (existingPath && existsSync(existingPath)) {\n const key = await this.load(existingPath);\n return { key, path: existingPath, isNew: false };\n }\n\n const chosenPath = promptForPath\n ? await promptForPath(this.defaultKeyPath)\n : this.defaultKeyPath;\n\n // A key already at the chosen path (e.g. the config was removed or never written) is\n // adopted rather than overwritten: overwriting it would break every MCP client already\n // configured with it.\n const adopted = await this.loadIfPresent(chosenPath);\n if (adopted) {\n await saveDRConfig({ ...config, mcp: { ...config.mcp, api_key_path: chosenPath } });\n return { key: adopted, path: chosenPath, isNew: false };\n }\n\n const key = this.generate();\n await this.store(chosenPath, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: chosenPath },\n });\n\n return { key, path: chosenPath, isNew: true };\n }\n\n /**\n * Force-generate a new API key, overwriting whatever is at the configured\n * (or default) storage path, regardless of whether a key already exists\n * there. Unlike `ensureKey`, this always writes a fresh key.\n *\n * @param promptForPath Called only when no storage path is yet configured,\n * to ask the user where to store it. When omitted, `defaultKeyPath` is\n * used without prompting.\n */\n async rotate(promptForPath?: ApiKeyStoragePrompt): Promise<RotateKeyResult> {\n const config = await this.loadConfigOrThrow();\n const path =\n config.mcp?.api_key_path ??\n (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath);\n\n const key = this.generate();\n await this.store(path, key);\n await saveDRConfig({\n ...config,\n mcp: { ...config.mcp, api_key_path: path },\n });\n\n return { key, path };\n }\n}\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,aAAa,uBAAuB;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAmB,oBAAoB,cAAc,oBAAoB;AAGzE,MAAM,gBAAgB;AAOhB,SAAU,uBAAoB;AAClC,SAAO,KAAK,QAAO,GAAI,aAAa;AACtC;AAiBM,MAAO,cAAa;EAMK;;;;;;EAA7B,YAA6B,iBAAyB,qBAAoB,GAAE;AAA/C,SAAA,iBAAA;EAAkD;;EAG/E,WAAQ;AACN,WAAO,UAAU,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;EACxD;;EAGA,MAAM,KAAK,MAAY;AAErB,UAAM,OADU,MAAM,SAAS,MAAM,OAAO,GACxB,KAAI;AAExB,QAAI,CAAC;AACH,YAAM,IAAI,MACR,mBAAmB,IAAI,gGACqD;AAIhF,WAAO;EACT;;EAGQ,MAAM,cAAc,MAAY;AACtC,QAAI,CAAC,WAAW,IAAI;AAAG,aAAO;AAC9B,QAAI;AACF,aAAO,MAAM,KAAK,KAAK,IAAI;IAC7B,QAAQ;AAEN,aAAO;IACT;EACF;;EAGA,MAAM,MAAM,MAAc,KAAW;AACnC,UAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,GAAI,CAAE,GAC9C,MAAM,UAAU,MAAM,GAAG,GAAG;GAAM,EAAE,MAAM,cAAa,CAAE,GAGzD,MAAM,MAAM,MAAM,aAAa;EACjC;;EAGA,SAAS,UAAqC,UAAgB;AAC5D,QAAI,CAAC;AAAU,aAAO;AAEtB,UAAM,cAAc,OAAO,KAAK,QAAQ,GAClC,cAAc,OAAO,KAAK,QAAQ;AACxC,WAAI,YAAY,WAAW,YAAY,SAAe,KAE/C,gBAAgB,aAAa,WAAW;EACjD;;;;;;;;;EAUQ,MAAM,oBAAiB;AAC7B,QAAI;AACF,aAAO,MAAM,aAAa,EAAE,QAAQ,GAAI,CAAE;IAC5C,SAAS,OAAO;AACd,YAAI,iBAAiB,qBACb,IAAI,MACR,qDAAqD,MAAM,UAAU,qJAGrE,EAAE,OAAO,MAAK,CAAE,IAGd;IACR;EACF;;;;;;;;;EAUA,MAAM,UAAU,eAAmC;AACjD,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,eAAe,OAAO,KAAK;AAEjC,QAAI,gBAAgB,WAAW,YAAY;AAEzC,aAAO,EAAE,KADG,MAAM,KAAK,KAAK,YAAY,GAC1B,MAAM,cAAc,OAAO,GAAK;AAGhD,UAAM,aAAa,gBACf,MAAM,cAAc,KAAK,cAAc,IACvC,KAAK,gBAKH,UAAU,MAAM,KAAK,cAAc,UAAU;AACnD,QAAI;AACF,mBAAM,aAAa,EAAE,GAAG,QAAQ,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,WAAU,EAAE,CAAE,GAC3E,EAAE,KAAK,SAAS,MAAM,YAAY,OAAO,GAAK;AAGvD,UAAM,MAAM,KAAK,SAAQ;AACzB,iBAAM,KAAK,MAAM,YAAY,GAAG,GAChC,MAAM,aAAa;MACjB,GAAG;MACH,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,WAAU;KAC/C,GAEM,EAAE,KAAK,MAAM,YAAY,OAAO,GAAI;EAC7C;;;;;;;;;;EAWA,MAAM,OAAO,eAAmC;AAC9C,UAAM,SAAS,MAAM,KAAK,kBAAiB,GACrC,OACJ,OAAO,KAAK,iBACX,gBAAgB,MAAM,cAAc,KAAK,cAAc,IAAI,KAAK,iBAE7D,MAAM,KAAK,SAAQ;AACzB,iBAAM,KAAK,MAAM,MAAM,GAAG,GAC1B,MAAM,aAAa;MACjB,GAAG;MACH,KAAK,EAAE,GAAG,OAAO,KAAK,cAAc,KAAI;KACzC,GAEM,EAAE,KAAK,KAAI;EACpB;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isChangesetOnly, CHANGESET_ONLY_ENV } from "../core/changeset-only-guard.js";
|
|
2
|
+
import { StagingAreaManager } from "../core/staging-area.js";
|
|
3
|
+
import { CLIError, ErrorCategory, getErrorMessage } from "../utils/errors.js";
|
|
4
|
+
import { activeStagedChangeset } from "./tools/changeset-shared.js";
|
|
5
|
+
const BASE_MODEL_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6
|
+
"model_add",
|
|
7
|
+
"model_update",
|
|
8
|
+
"model_delete",
|
|
9
|
+
"relationship_add",
|
|
10
|
+
"relationship_delete"
|
|
11
|
+
]);
|
|
12
|
+
function changesetOnlyError(toolName) {
|
|
13
|
+
const retry = toolName ? `Then call "${toolName}" again` : "Then retry the call";
|
|
14
|
+
return new CLIError("This session can only change the model through a changeset, and no changeset is active.", ErrorCategory.USER, [
|
|
15
|
+
'Call "changeset_create" with a name for the change (it activates the new changeset), or "changeset_activate" to resume one from "changeset_list"',
|
|
16
|
+
`${retry}: it will be staged in the changeset for the user to review and commit`
|
|
17
|
+
], { operation: toolName ?? "save model", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? "true"}` });
|
|
18
|
+
}
|
|
19
|
+
async function changesetOnlyRefusal(toolName, rootPath, loadModel, env = process.env) {
|
|
20
|
+
if (!isChangesetOnly(env) || !BASE_MODEL_WRITE_TOOLS.has(toolName))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const model = await loadModel(rootPath);
|
|
24
|
+
return await activeStagedChangeset(new StagingAreaManager(model.rootPath, model)) ? null : changesetOnlyError(toolName);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`), null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
BASE_MODEL_WRITE_TOOLS,
|
|
31
|
+
changesetOnlyError,
|
|
32
|
+
changesetOnlyRefusal
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=changeset-only.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/changeset-only.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Changeset-only mode on the MCP server.\n *\n * When `dr mcp` runs with DR_CHANGESET_ONLY=true (DrBot under `dr visualize --with-changesets`\n * starts it that way), an agent may change the model only by staging into an active changeset.\n * The tools below write the base model when no changeset is active, so the tool registry asks\n * `changesetOnlyRefusal()` before running them and returns its refusal instead: the agent is told\n * to call changeset_create (or changeset_activate) and retry.\n *\n * This check runs before the handler so a refused call never touches the cached in-memory model.\n * Any other base-model save still hits the core guard (core/changeset-only-guard.ts), whose\n * ChangesetOnlyError runTool turns into the same refusal, after dropping the model cache so a\n * half-applied in-memory edit can't linger.\n */\n\nimport type { Model } from \"../core/model.js\";\nimport { isChangesetOnly, CHANGESET_ONLY_ENV } from \"../core/changeset-only-guard.js\";\nimport { StagingAreaManager } from \"../core/staging-area.js\";\nimport { CLIError, ErrorCategory, getErrorMessage } from \"../utils/errors.js\";\nimport { activeStagedChangeset } from \"./tools/changeset-shared.js\";\n\n/** Tools that write the base model when no changeset is active. */\nexport const BASE_MODEL_WRITE_TOOLS: ReadonlySet<string> = new Set([\n \"model_add\",\n \"model_update\",\n \"model_delete\",\n \"relationship_add\",\n \"relationship_delete\",\n]);\n\n/** The error an agent gets for a base-model write in changeset-only mode. */\nexport function changesetOnlyError(toolName?: string): CLIError {\n const retry = toolName ? `Then call \"${toolName}\" again` : \"Then retry the call\";\n return new CLIError(\n \"This session can only change the model through a changeset, and no changeset is active.\",\n ErrorCategory.USER,\n [\n 'Call \"changeset_create\" with a name for the change (it activates the new changeset), or ' +\n '\"changeset_activate\" to resume one from \"changeset_list\"',\n `${retry}: it will be staged in the changeset for the user to review and commit`,\n ],\n { operation: toolName ?? \"save model\", context: `${CHANGESET_ONLY_ENV}=${process.env[CHANGESET_ONLY_ENV] ?? \"true\"}` }\n );\n}\n\n/**\n * The refusal for this call, or null when it may run: always null outside changeset-only mode,\n * for tools that don't write the base model, and when a staged changeset is active.\n *\n * If the model or the active changeset can't be read, the check steps aside (null, logged) and\n * the tool reports that failure itself; the core guard still stops any base-model save.\n */\nexport async function changesetOnlyRefusal(\n toolName: string,\n rootPath: string | undefined,\n loadModel: (rootPath?: string) => Promise<Model>,\n env: NodeJS.ProcessEnv = process.env\n): Promise<CLIError | null> {\n if (!isChangesetOnly(env) || !BASE_MODEL_WRITE_TOOLS.has(toolName)) {\n return null;\n }\n try {\n const model = await loadModel(rootPath);\n const active = await activeStagedChangeset(new StagingAreaManager(model.rootPath, model));\n return active ? null : changesetOnlyError(toolName);\n } catch (error) {\n console.error(`[mcp] changeset-only check for ${toolName} skipped: ${getErrorMessage(error)}`);\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,iBAAiB,0BAA0B;AACpD,SAAS,0BAA0B;AACnC,SAAS,UAAU,eAAe,uBAAuB;AACzD,SAAS,6BAA6B;AAG/B,MAAM,yBAA8C,oBAAI,IAAI;EACjE;EACA;EACA;EACA;EACA;CACD;AAGK,SAAU,mBAAmB,UAAiB;AAClD,QAAM,QAAQ,WAAW,cAAc,QAAQ,YAAY;AAC3D,SAAO,IAAI,SACT,2FACA,cAAc,MACd;IACE;IAEA,GAAG,KAAK;KAEV,EAAE,WAAW,YAAY,cAAc,SAAS,GAAG,kBAAkB,IAAI,QAAQ,IAAI,kBAAkB,KAAK,MAAM,GAAE,CAAE;AAE1H;AASA,eAAsB,qBACpB,UACA,UACA,WACA,MAAyB,QAAQ,KAAG;AAEpC,MAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,uBAAuB,IAAI,QAAQ;AAC/D,WAAO;AAET,MAAI;AACF,UAAM,QAAQ,MAAM,UAAU,QAAQ;AAEtC,WADe,MAAM,sBAAsB,IAAI,mBAAmB,MAAM,UAAU,KAAK,CAAC,IACxE,OAAO,mBAAmB,QAAQ;EACpD,SAAS,OAAO;AACd,mBAAQ,MAAM,kCAAkC,QAAQ,aAAa,gBAAgB,KAAK,CAAC,EAAE,GACtF;EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
function createBearerAuthMiddleware(keyManager, expectedKey) {
|
|
7
|
+
return (req, res, next) => {
|
|
8
|
+
const match = (req.get("Authorization") || "").match(/^Bearer\s+(.+)$/), providedKey = match ? match[1] : void 0;
|
|
9
|
+
if (!keyManager.validate(providedKey, expectedKey)) {
|
|
10
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
next();
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function createMcpHttpApp(keyManager, apiKey, createServer, host = "127.0.0.1") {
|
|
17
|
+
const app = createMcpExpressApp({ host });
|
|
18
|
+
app.__mcp_sessions = /* @__PURE__ */ new Map();
|
|
19
|
+
const sessions = app.__mcp_sessions;
|
|
20
|
+
return app.use(express.raw({ type: "application/octet-stream" })), app.use("/mcp", createBearerAuthMiddleware(keyManager, apiKey)), app.post("/mcp", async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const requestedSessionId = req.headers["mcp-session-id"];
|
|
23
|
+
if (requestedSessionId && sessions.has(requestedSessionId)) {
|
|
24
|
+
const sessionData = sessions.get(requestedSessionId);
|
|
25
|
+
if (sessionData) {
|
|
26
|
+
try {
|
|
27
|
+
await sessionData.transport.handleRequest(req, res, req.body);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const errorMsg = error instanceof Error ? error.message : "unknown error";
|
|
30
|
+
process.stderr.write(`[mcp:http] POST handleRequest error: ${errorMsg}
|
|
31
|
+
`), res.headersSent || res.status(500).json({ error: "Internal server error" });
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!isInitializeRequest(req.body)) {
|
|
37
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Invalid Request: initialize required for new session" }, id: null });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let server = null, transport = null;
|
|
41
|
+
try {
|
|
42
|
+
server = await createServer(), transport = new StreamableHTTPServerTransport({
|
|
43
|
+
sessionIdGenerator: () => randomUUID()
|
|
44
|
+
}), transport.onclose = async () => {
|
|
45
|
+
if (transport && transport.sessionId) {
|
|
46
|
+
const sessionId2 = transport.sessionId, sessionData = sessions.get(sessionId2);
|
|
47
|
+
if (sessionData) {
|
|
48
|
+
sessions.delete(sessionId2);
|
|
49
|
+
try {
|
|
50
|
+
await sessionData.server.close();
|
|
51
|
+
} catch (closeError) {
|
|
52
|
+
process.stderr.write(`[mcp:http] Error closing server in onclose handler: ${closeError instanceof Error ? closeError.message : "unknown error"}
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, await server.connect(transport), await transport.handleRequest(req, res, req.body);
|
|
58
|
+
const sessionId = transport.sessionId;
|
|
59
|
+
sessionId && !sessions.has(sessionId) && sessions.set(sessionId, { server, transport });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const errorMsg = error instanceof Error ? error.message : "unknown error";
|
|
62
|
+
if (process.stderr.write(`[mcp:http] POST setup error: ${errorMsg}
|
|
63
|
+
`), transport)
|
|
64
|
+
try {
|
|
65
|
+
await transport.close();
|
|
66
|
+
} catch (closeError) {
|
|
67
|
+
process.stderr.write(`[mcp:http] Error closing transport during cleanup: ${closeError instanceof Error ? closeError.message : "unknown error"}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
if (server)
|
|
71
|
+
try {
|
|
72
|
+
await server.close();
|
|
73
|
+
} catch (closeError) {
|
|
74
|
+
process.stderr.write(`[mcp:http] Error closing server during cleanup: ${closeError instanceof Error ? closeError.message : "unknown error"}
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
res.headersSent || res.status(500).json({ error: "Internal server error" });
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const errorMsg = error instanceof Error ? error.message : "unknown error";
|
|
81
|
+
process.stderr.write(`[mcp:http] POST outer error: ${errorMsg}
|
|
82
|
+
`), res.headersSent || res.status(500).json({ error: "Internal server error" });
|
|
83
|
+
}
|
|
84
|
+
}), app.get("/mcp", async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
87
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
88
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sessionData = sessions.get(sessionId);
|
|
92
|
+
if (!sessionData) {
|
|
93
|
+
res.status(400).json({ error: "Session not found" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await sessionData.transport.handleRequest(req, res);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const errorMsg = error instanceof Error ? error.message : "unknown error";
|
|
99
|
+
process.stderr.write(`[mcp:http] GET handleRequest error: ${errorMsg}
|
|
100
|
+
`), res.headersSent || res.status(500).json({ error: "Internal server error" });
|
|
101
|
+
}
|
|
102
|
+
}), app.delete("/mcp", async (req, res) => {
|
|
103
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
104
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
105
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const sessionData = sessions.get(sessionId);
|
|
109
|
+
if (!sessionData) {
|
|
110
|
+
res.status(400).json({ error: "Session not found" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await sessionData.transport.handleRequest(req, res);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const errorMsg = error instanceof Error ? error.message : "unknown error";
|
|
117
|
+
process.stderr.write(`[mcp:http] DELETE handleRequest error: ${errorMsg}
|
|
118
|
+
`), res.headersSent || res.status(500).json({ error: "Internal server error" });
|
|
119
|
+
} finally {
|
|
120
|
+
if (sessions.has(sessionId)) {
|
|
121
|
+
sessions.delete(sessionId);
|
|
122
|
+
try {
|
|
123
|
+
await sessionData.server.close();
|
|
124
|
+
} catch (closeError) {
|
|
125
|
+
process.stderr.write(`[mcp:http] Error closing server in DELETE handler: ${closeError instanceof Error ? closeError.message : "unknown error"}
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await sessionData.transport.close();
|
|
130
|
+
} catch (closeError) {
|
|
131
|
+
process.stderr.write(`[mcp:http] Error closing transport in DELETE handler: ${closeError instanceof Error ? closeError.message : "unknown error"}
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}), app;
|
|
137
|
+
}
|
|
138
|
+
async function startHttpServer(app, host, port) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const server = app.listen(port, host, () => {
|
|
141
|
+
resolve(server);
|
|
142
|
+
});
|
|
143
|
+
server.once("error", reject);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function closeHttpServer(server, app) {
|
|
147
|
+
const sessions = app?.__mcp_sessions;
|
|
148
|
+
if (sessions)
|
|
149
|
+
for (const [sessionId, { server: mcpServer, transport }] of sessions.entries()) {
|
|
150
|
+
try {
|
|
151
|
+
await mcpServer.close();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
process.stderr.write(`[mcp] Error closing server for session ${sessionId}: ${error instanceof Error ? error.message : "unknown error"}
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
sessions.delete(sessionId);
|
|
157
|
+
try {
|
|
158
|
+
await transport.close();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
process.stderr.write(`[mcp] Error closing transport for session ${sessionId}: ${error instanceof Error ? error.message : "unknown error"}
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
server.close((error) => {
|
|
166
|
+
error ? reject(error) : resolve();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
closeHttpServer,
|
|
172
|
+
createBearerAuthMiddleware,
|
|
173
|
+
createMcpHttpApp,
|
|
174
|
+
startHttpServer
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=http-transport.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/http-transport.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * HTTP transport implementation for MCP server\n *\n * Provides an Express-based HTTP server exposing the MCP protocol over a /mcp endpoint\n * with per-session server/transport handling and bearer token authentication.\n */\n\nimport { Server as HttpServer } from \"http\";\nimport { randomUUID } from \"node:crypto\";\nimport express, { Express, Request, Response, NextFunction } from \"express\";\nimport { createMcpExpressApp } from \"@modelcontextprotocol/sdk/server/express.js\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport { ApiKeyManager } from \"./api-key-manager.js\";\n\ninterface SessionData {\n server: McpServer;\n transport: StreamableHTTPServerTransport;\n}\n\nexport interface HttpTransportApp extends Express {\n __mcp_sessions?: Map<string, SessionData>;\n}\n\n/**\n * Middleware to validate bearer token authentication.\n * Extracts and validates the Authorization header, rejecting with 401 if invalid.\n */\nexport function createBearerAuthMiddleware(keyManager: ApiKeyManager, expectedKey: string) {\n return (req: Request, res: Response, next: NextFunction): void => {\n const authHeader = req.get(\"Authorization\") || \"\";\n const match = authHeader.match(/^Bearer\\s+(.+)$/);\n const providedKey = match ? match[1] : undefined;\n\n if (!keyManager.validate(providedKey, expectedKey)) {\n res.status(401).json({ error: \"Unauthorized\" });\n return;\n }\n\n next();\n };\n}\n\n/**\n * Create an Express application configured for MCP HTTP transport.\n *\n * @param keyManager API key manager for authentication\n * @param apiKey The API key to authenticate requests\n * @param createServer Async function to create a configured MCP server\n * @param host Host address the server will bind to (used for DNS rebinding protection)\n */\nexport async function createMcpHttpApp(\n keyManager: ApiKeyManager,\n apiKey: string,\n createServer: () => Promise<McpServer>,\n host: string = \"127.0.0.1\"\n): Promise<HttpTransportApp> {\n const app = createMcpExpressApp({ host }) as HttpTransportApp;\n app.__mcp_sessions = new Map<string, SessionData>();\n const sessions = app.__mcp_sessions;\n\n // Middleware\n app.use(express.raw({ type: \"application/octet-stream\" }));\n\n // Apply bearer token authentication to all MCP endpoints\n app.use(\"/mcp\", createBearerAuthMiddleware(keyManager, apiKey));\n\n // Handle POST requests for MCP protocol\n app.post(\"/mcp\", async (req: Request, res: Response) => {\n try {\n // Extract session ID from request headers if provided\n const requestedSessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n // Check if this is a continuation of an existing session\n if (requestedSessionId && sessions.has(requestedSessionId)) {\n // Reuse existing session\n const sessionData = sessions.get(requestedSessionId);\n if (sessionData) {\n try {\n await sessionData.transport.handleRequest(req, res, req.body);\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp:http] POST handleRequest error: ${errorMsg}\\n`);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Internal server error\" });\n }\n }\n return;\n }\n }\n\n // Only create a new session for initialize requests\n if (!isInitializeRequest(req.body)) {\n res.status(400).json({ jsonrpc: \"2.0\", error: { code: -32600, message: \"Invalid Request: initialize required for new session\" }, id: null });\n return;\n }\n\n // Create a new session\n let server: McpServer | null = null;\n let transport: StreamableHTTPServerTransport | null = null;\n\n try {\n server = await createServer();\n transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n\n // Register onclose handler to clean up session when transport closes\n transport.onclose = async () => {\n if (transport && transport.sessionId) {\n const sessionId = transport.sessionId;\n const sessionData = sessions.get(sessionId);\n if (sessionData) {\n sessions.delete(sessionId);\n // Close server to release resources\n try {\n await sessionData.server.close();\n } catch (closeError) {\n process.stderr.write(\n `[mcp:http] Error closing server in onclose handler: ${\n closeError instanceof Error ? closeError.message : \"unknown error\"\n }\\n`\n );\n }\n }\n }\n };\n\n // Connect server to transport before handling the request\n await server.connect(transport);\n\n // Handle the request through the streamable HTTP transport\n await transport.handleRequest(req, res, req.body);\n\n // Store session data for later cleanup after handleRequest completes and sessionId is set\n const sessionId = transport.sessionId;\n if (sessionId && !sessions.has(sessionId)) {\n sessions.set(sessionId, { server, transport });\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp:http] POST setup error: ${errorMsg}\\n`);\n\n // Clean up resources on setup failure\n if (transport) {\n try {\n await transport.close();\n } catch (closeError) {\n process.stderr.write(\n `[mcp:http] Error closing transport during cleanup: ${\n closeError instanceof Error ? closeError.message : \"unknown error\"\n }\\n`\n );\n }\n }\n if (server) {\n try {\n await server.close();\n } catch (closeError) {\n process.stderr.write(\n `[mcp:http] Error closing server during cleanup: ${\n closeError instanceof Error ? closeError.message : \"unknown error\"\n }\\n`\n );\n }\n }\n\n if (!res.headersSent) {\n res.status(500).json({ error: \"Internal server error\" });\n }\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp:http] POST outer error: ${errorMsg}\\n`);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Internal server error\" });\n }\n }\n });\n\n // Handle GET requests for SSE streams\n app.get(\"/mcp\", async (req: Request, res: Response) => {\n try {\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).json({ error: \"Invalid or missing session ID\" });\n return;\n }\n\n const sessionData = sessions.get(sessionId);\n if (!sessionData) {\n res.status(400).json({ error: \"Session not found\" });\n return;\n }\n\n // Reuse the existing transport for this session\n await sessionData.transport.handleRequest(req, res);\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp:http] GET handleRequest error: ${errorMsg}\\n`);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Internal server error\" });\n }\n }\n });\n\n // Handle DELETE requests for session termination\n app.delete(\"/mcp\", async (req: Request, res: Response) => {\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).json({ error: \"Invalid or missing session ID\" });\n return;\n }\n\n const sessionData = sessions.get(sessionId);\n if (!sessionData) {\n res.status(400).json({ error: \"Session not found\" });\n return;\n }\n\n try {\n // Handle the DELETE request through the transport\n await sessionData.transport.handleRequest(req, res);\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"unknown error\";\n process.stderr.write(`[mcp:http] DELETE handleRequest error: ${errorMsg}\\n`);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Internal server error\" });\n }\n } finally {\n // Always clean up the session, even if handleRequest throws\n if (sessions.has(sessionId)) {\n sessions.delete(sessionId);\n // Close server to release resources\n try {\n await sessionData.server.close();\n } catch (closeError) {\n process.stderr.write(\n `[mcp:http] Error closing server in DELETE handler: ${\n closeError instanceof Error ? closeError.message : \"unknown error\"\n }\\n`\n );\n }\n // Close transport to release resources\n try {\n await sessionData.transport.close();\n } catch (closeError) {\n process.stderr.write(\n `[mcp:http] Error closing transport in DELETE handler: ${\n closeError instanceof Error ? closeError.message : \"unknown error\"\n }\\n`\n );\n }\n }\n }\n });\n\n return app;\n}\n\n/**\n * Start the HTTP server and return the listening HttpServer instance.\n *\n * @param app Express application\n * @param host Host address to bind to\n * @param port Port to bind to\n * @returns HttpServer instance when listening\n */\nexport async function startHttpServer(\n app: Express,\n host: string,\n port: number\n): Promise<HttpServer> {\n return new Promise((resolve, reject) => {\n const server = app.listen(port, host, () => {\n resolve(server);\n });\n\n server.once(\"error\", reject);\n });\n}\n\n/**\n * Gracefully close all active sessions and the HTTP server.\n *\n * @param server HttpServer instance\n * @param app Express application (used to access instance-scoped sessions)\n */\nexport async function closeHttpServer(\n server: HttpServer,\n app?: HttpTransportApp\n): Promise<void> {\n const sessions = app?.__mcp_sessions;\n\n if (sessions) {\n // Close all active sessions\n for (const [sessionId, { server: mcpServer, transport }] of sessions.entries()) {\n try {\n // Close the server to release resources\n await mcpServer.close();\n } catch (error) {\n process.stderr.write(\n `[mcp] Error closing server for session ${sessionId}: ${\n error instanceof Error ? error.message : \"unknown error\"\n }\\n`\n );\n }\n // Remove from sessions map BEFORE closing transport to prevent the onclose handler\n // from finding the session and trying to close the server again\n sessions.delete(sessionId);\n try {\n // Close the transport\n await transport.close();\n } catch (error) {\n process.stderr.write(\n `[mcp] Error closing transport for session ${sessionId}: ${\n error instanceof Error ? error.message : \"unknown error\"\n }\\n`\n );\n }\n }\n }\n\n // Close the HTTP server\n return new Promise((resolve, reject) => {\n server.close((error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["sessionId"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { AnnotationStore } from "../core/annotation-store.js";
|
|
5
|
+
import { CANONICAL_LAYER_NAMES } from "../core/layers.js";
|
|
6
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
7
|
+
import { loadModel } from "./tools/shared.js";
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url)), BUNDLED_SCHEMAS_DIR = path.join(__dirname, "../schemas/bundled");
|
|
9
|
+
async function readBundledSchema(fileName) {
|
|
10
|
+
try {
|
|
11
|
+
return await fs.readFile(path.join(BUNDLED_SCHEMAS_DIR, fileName), "utf-8");
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw error instanceof Error && "code" in error && error.code === "ENOENT" ? new Error(`Bundled schema "${fileName}" is missing from ${BUNDLED_SCHEMAS_DIR}. This usually means the CLI build is incomplete or corrupted \u2014 run "npm run build" in cli/ to regenerate it.`) : error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class McpResourceRegistry {
|
|
17
|
+
/** Registers every spec/model resource onto the given MCP server instance. */
|
|
18
|
+
async registerAll(server) {
|
|
19
|
+
const { ResourceTemplate } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
20
|
+
server.registerResource("spec-manifest", "dr://spec/manifest", {
|
|
21
|
+
title: "Specification manifest",
|
|
22
|
+
description: "Index of all 12 architecture layers in the compiled specification.",
|
|
23
|
+
mimeType: "application/json"
|
|
24
|
+
}, async (uri) => {
|
|
25
|
+
const text = await readBundledSchema("manifest.json");
|
|
26
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text }] };
|
|
27
|
+
}), server.registerResource("spec-base", "dr://spec/base", {
|
|
28
|
+
title: "Base specification schemas",
|
|
29
|
+
description: "Base schemas and predicates shared across all layers.",
|
|
30
|
+
mimeType: "application/json"
|
|
31
|
+
}, async (uri) => {
|
|
32
|
+
const text = await readBundledSchema("base.json");
|
|
33
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text }] };
|
|
34
|
+
}), server.registerResource("spec-layer", new ResourceTemplate("dr://spec/layer/{name}", {
|
|
35
|
+
list: async () => ({
|
|
36
|
+
resources: CANONICAL_LAYER_NAMES.map((name) => ({
|
|
37
|
+
name: `spec-layer-${name}`,
|
|
38
|
+
uri: `dr://spec/layer/${name}`,
|
|
39
|
+
title: `${name} layer schemas`,
|
|
40
|
+
mimeType: "application/json"
|
|
41
|
+
}))
|
|
42
|
+
})
|
|
43
|
+
}), {
|
|
44
|
+
title: "Layer specification schemas",
|
|
45
|
+
description: "Node and relationship schemas for a single architecture layer.",
|
|
46
|
+
mimeType: "application/json"
|
|
47
|
+
}, async (uri, variables) => {
|
|
48
|
+
const name = String(variables.name);
|
|
49
|
+
if (!CANONICAL_LAYER_NAMES.includes(name))
|
|
50
|
+
throw new Error(`Unknown spec layer "${name}". Valid layers: ${CANONICAL_LAYER_NAMES.join(", ")}`);
|
|
51
|
+
const text = await readBundledSchema(`${name}.json`);
|
|
52
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text }] };
|
|
53
|
+
}), server.registerResource("model-manifest", "dr://model/manifest", {
|
|
54
|
+
title: "Model manifest",
|
|
55
|
+
description: "Metadata and layer summary for the current architecture model.",
|
|
56
|
+
mimeType: "application/json"
|
|
57
|
+
}, async (uri) => {
|
|
58
|
+
try {
|
|
59
|
+
const model = await loadModel(), manifest = {
|
|
60
|
+
name: model.manifest.name,
|
|
61
|
+
version: model.manifest.version,
|
|
62
|
+
specVersion: model.manifest.specVersion,
|
|
63
|
+
description: model.manifest.description,
|
|
64
|
+
author: model.manifest.author,
|
|
65
|
+
created: model.manifest.created,
|
|
66
|
+
modified: model.manifest.modified,
|
|
67
|
+
layers: model.getLayerNames()
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(manifest, null, 2) }]
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Failed to load model manifest: ${getErrorMessage(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}), server.registerResource("model-annotations", "dr://model/annotations", {
|
|
76
|
+
title: "Model annotations",
|
|
77
|
+
description: "All annotations on elements in the current architecture model.",
|
|
78
|
+
mimeType: "application/json"
|
|
79
|
+
}, async (uri) => {
|
|
80
|
+
try {
|
|
81
|
+
const model = await loadModel(), annotations = await new AnnotationStore(model.rootPath).list();
|
|
82
|
+
return {
|
|
83
|
+
contents: [
|
|
84
|
+
{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ annotations }, null, 2) }
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new Error(`Failed to load model annotations: ${getErrorMessage(error)}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
McpResourceRegistry,
|
|
95
|
+
readBundledSchema
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=resource-registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/resource-registry.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * McpResourceRegistry \u2014 registers spec and model manifest resources onto an\n * MCP server.\n *\n * - dr://spec/manifest \u2014 index of all 12 compiled specification layers\n * - dr://spec/base \u2014 base schemas + predicates shared across layers\n * - dr://spec/layer/{name} \u2014 node/relationship schemas for a single layer\n * - dr://model/manifest \u2014 metadata and layer summary for the current model\n * - dr://model/annotations \u2014 all annotations across the current model\n */\n\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { AnnotationStore } from \"../core/annotation-store.js\";\nimport { CANONICAL_LAYER_NAMES } from \"../core/layers.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { loadModel } from \"./tools/shared.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst BUNDLED_SCHEMAS_DIR = path.join(__dirname, \"../schemas/bundled\");\n\n/** Exported for direct unit testing of the ENOENT-to-actionable-error translation. */\nexport async function readBundledSchema(fileName: string): Promise<string> {\n try {\n return await fs.readFile(path.join(BUNDLED_SCHEMAS_DIR, fileName), \"utf-8\");\n } catch (error) {\n if (error instanceof Error && \"code\" in error && (error as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new Error(\n `Bundled schema \"${fileName}\" is missing from ${BUNDLED_SCHEMAS_DIR}. ` +\n `This usually means the CLI build is incomplete or corrupted \u2014 run \"npm run build\" in cli/ to regenerate it.`\n );\n }\n throw error;\n }\n}\n\nexport class McpResourceRegistry {\n /** Registers every spec/model resource onto the given MCP server instance. */\n async registerAll(server: McpServer): Promise<void> {\n const { ResourceTemplate } = await import(\"@modelcontextprotocol/sdk/server/mcp.js\");\n\n server.registerResource(\n \"spec-manifest\",\n \"dr://spec/manifest\",\n {\n title: \"Specification manifest\",\n description: \"Index of all 12 architecture layers in the compiled specification.\",\n mimeType: \"application/json\",\n },\n async (uri) => {\n const text = await readBundledSchema(\"manifest.json\");\n return { contents: [{ uri: uri.href, mimeType: \"application/json\", text }] };\n }\n );\n\n server.registerResource(\n \"spec-base\",\n \"dr://spec/base\",\n {\n title: \"Base specification schemas\",\n description: \"Base schemas and predicates shared across all layers.\",\n mimeType: \"application/json\",\n },\n async (uri) => {\n const text = await readBundledSchema(\"base.json\");\n return { contents: [{ uri: uri.href, mimeType: \"application/json\", text }] };\n }\n );\n\n server.registerResource(\n \"spec-layer\",\n new ResourceTemplate(\"dr://spec/layer/{name}\", {\n list: async () => ({\n resources: CANONICAL_LAYER_NAMES.map((name) => ({\n name: `spec-layer-${name}`,\n uri: `dr://spec/layer/${name}`,\n title: `${name} layer schemas`,\n mimeType: \"application/json\",\n })),\n }),\n }),\n {\n title: \"Layer specification schemas\",\n description: \"Node and relationship schemas for a single architecture layer.\",\n mimeType: \"application/json\",\n },\n async (uri, variables) => {\n const name = String(variables.name);\n if (!(CANONICAL_LAYER_NAMES as readonly string[]).includes(name)) {\n throw new Error(\n `Unknown spec layer \"${name}\". Valid layers: ${CANONICAL_LAYER_NAMES.join(\", \")}`\n );\n }\n const text = await readBundledSchema(`${name}.json`);\n return { contents: [{ uri: uri.href, mimeType: \"application/json\", text }] };\n }\n );\n\n server.registerResource(\n \"model-manifest\",\n \"dr://model/manifest\",\n {\n title: \"Model manifest\",\n description: \"Metadata and layer summary for the current architecture model.\",\n mimeType: \"application/json\",\n },\n async (uri) => {\n try {\n const model = await loadModel();\n const manifest = {\n name: model.manifest.name,\n version: model.manifest.version,\n specVersion: model.manifest.specVersion,\n description: model.manifest.description,\n author: model.manifest.author,\n created: model.manifest.created,\n modified: model.manifest.modified,\n layers: model.getLayerNames(),\n };\n return {\n contents: [{ uri: uri.href, mimeType: \"application/json\", text: JSON.stringify(manifest, null, 2) }],\n };\n } catch (error) {\n throw new Error(`Failed to load model manifest: ${getErrorMessage(error)}`);\n }\n }\n );\n\n server.registerResource(\n \"model-annotations\",\n \"dr://model/annotations\",\n {\n title: \"Model annotations\",\n description: \"All annotations on elements in the current architecture model.\",\n mimeType: \"application/json\",\n },\n async (uri) => {\n try {\n const model = await loadModel();\n const store = new AnnotationStore(model.rootPath);\n const annotations = await store.list();\n return {\n contents: [\n { uri: uri.href, mimeType: \"application/json\", text: JSON.stringify({ annotations }, null, 2) },\n ],\n };\n } catch (error) {\n throw new Error(`Failed to load model annotations: ${getErrorMessage(error)}`);\n }\n }\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isTelemetryEnabled, startActiveSpan } from "../telemetry/index.js";
|
|
2
|
+
import { annotationCreateTool, annotationDeleteTool, annotationGetTool, annotationListTool, annotationReplyTool, annotationUpdateTool, changesetActivateTool, changesetBriefTool, changesetCreateTool, changesetDeactivateTool, changesetImpactTool, changesetListTool, changesetShowTool, chatLaunchTool, chatStatusTool, modelAddTool, modelDeleteTool, modelExportTool, modelInfoTool, modelListTool, modelReloadTool, modelSearchTool, modelShowTool, modelStatsTool, modelTraceTool, modelUpdateTool, modelValidateTool, relationshipAddTool, relationshipDeleteTool, specTypeTool } from "./tools/index.js";
|
|
3
|
+
import { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from "./tools/index.js";
|
|
4
|
+
import { changesetVerifyTool } from "./tools/index.js";
|
|
5
|
+
import { errorResult, loadModel } from "./tools/shared.js";
|
|
6
|
+
import { changesetOnlyRefusal } from "./changeset-only.js";
|
|
7
|
+
const MODEL_TOOLS = [
|
|
8
|
+
modelListTool,
|
|
9
|
+
modelShowTool,
|
|
10
|
+
modelSearchTool,
|
|
11
|
+
modelStatsTool,
|
|
12
|
+
modelInfoTool,
|
|
13
|
+
modelTraceTool,
|
|
14
|
+
modelAddTool,
|
|
15
|
+
modelUpdateTool,
|
|
16
|
+
modelDeleteTool,
|
|
17
|
+
modelValidateTool,
|
|
18
|
+
modelExportTool,
|
|
19
|
+
modelReloadTool,
|
|
20
|
+
changesetListTool,
|
|
21
|
+
changesetShowTool,
|
|
22
|
+
changesetCreateTool,
|
|
23
|
+
changesetActivateTool,
|
|
24
|
+
changesetDeactivateTool,
|
|
25
|
+
relationshipAddTool,
|
|
26
|
+
relationshipDeleteTool,
|
|
27
|
+
specTypeTool,
|
|
28
|
+
changesetImpactTool,
|
|
29
|
+
changesetBriefTool,
|
|
30
|
+
chatStatusTool,
|
|
31
|
+
chatLaunchTool,
|
|
32
|
+
annotationListTool,
|
|
33
|
+
annotationGetTool,
|
|
34
|
+
annotationCreateTool,
|
|
35
|
+
annotationUpdateTool,
|
|
36
|
+
annotationDeleteTool,
|
|
37
|
+
annotationReplyTool,
|
|
38
|
+
changesetUpdateTool,
|
|
39
|
+
changesetUnstageTool,
|
|
40
|
+
changesetDiscardTool,
|
|
41
|
+
changesetVerifyTool
|
|
42
|
+
];
|
|
43
|
+
class McpToolRegistry {
|
|
44
|
+
/** Registers every model tool onto the given MCP server instance. */
|
|
45
|
+
registerAll(server) {
|
|
46
|
+
for (const tool of MODEL_TOOLS)
|
|
47
|
+
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args) => startActiveSpan(`mcp.tool.${tool.name}`, async (span) => {
|
|
48
|
+
const refusal = await changesetOnlyRefusal(tool.name, args.rootPath, loadModel), result = refusal ? errorResult(refusal) : await tool.handler(args);
|
|
49
|
+
if (isTelemetryEnabled && result?.isError) {
|
|
50
|
+
const firstContent = result.content?.[0], message = firstContent?.type === "text" ? firstContent.text : "Tool returned an error";
|
|
51
|
+
span.setStatus({ code: 2, message });
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}, { "mcp.tool.name": tool.name }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
McpToolRegistry
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=tool-registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mcp/tool-registry.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * McpToolRegistry \u2014 registers the DR model tool surface onto an MCP server.\n *\n * Read-only tools: model_list, model_show, model_search, model_stats,\n * model_info, model_trace.\n * Mutation tools: model_add, model_update, model_delete (via MutationHandler).\n * Pipeline tools: model_validate, model_export (via the shared validator\n * pipeline and export handlers).\n * Lifecycle tools: model_reload (force a full re-read from disk).\n * Changeset tools: changeset_list, changeset_show (via StagingAreaManager).\n * Change-modeling tools (docs/CHANGE_MODELING.md): changeset_create, changeset_activate,\n * changeset_deactivate, relationship_add, relationship_delete, spec_type, changeset_impact,\n * changeset_brief, changeset_verify. Commit and apply are deliberately not exposed: landing a change\n * is the human's step.\n * Changeset editing (changeset-editing.ts): changeset_update, changeset_unstage, and\n * changeset_discard (requires confirm: true; it deletes the staged changes).\n * Chat tools: chat_status, chat_launch (via the coding-agents chat clients).\n * Annotation tools: annotation_list, annotation_get, annotation_create,\n * annotation_update, annotation_delete, annotation_reply (via AnnotationStore),\n * mirroring the REST server's /api/annotations/* endpoints.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { isTelemetryEnabled, startActiveSpan } from \"../telemetry/index.js\";\nimport {\n annotationCreateTool,\n annotationDeleteTool,\n annotationGetTool,\n annotationListTool,\n annotationReplyTool,\n annotationUpdateTool,\n changesetActivateTool,\n changesetBriefTool,\n changesetCreateTool,\n changesetDeactivateTool,\n changesetImpactTool,\n changesetListTool,\n changesetShowTool,\n chatLaunchTool,\n chatStatusTool,\n modelAddTool,\n modelDeleteTool,\n modelExportTool,\n modelInfoTool,\n modelListTool,\n modelReloadTool,\n modelSearchTool,\n modelShowTool,\n modelStatsTool,\n modelTraceTool,\n modelUpdateTool,\n modelValidateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n type McpToolDefinition,\n} from \"./tools/index.js\";\nimport { changesetDiscardTool, changesetUnstageTool, changesetUpdateTool } from \"./tools/index.js\";\nimport { changesetVerifyTool } from \"./tools/index.js\";\nimport { errorResult, loadModel } from \"./tools/shared.js\";\nimport { changesetOnlyRefusal } from \"./changeset-only.js\";\n\nconst MODEL_TOOLS: McpToolDefinition[] = [\n modelListTool,\n modelShowTool,\n modelSearchTool,\n modelStatsTool,\n modelInfoTool,\n modelTraceTool,\n modelAddTool,\n modelUpdateTool,\n modelDeleteTool,\n modelValidateTool,\n modelExportTool,\n modelReloadTool,\n changesetListTool,\n changesetShowTool,\n changesetCreateTool,\n changesetActivateTool,\n changesetDeactivateTool,\n relationshipAddTool,\n relationshipDeleteTool,\n specTypeTool,\n changesetImpactTool,\n changesetBriefTool,\n chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\n changesetUpdateTool,\n changesetUnstageTool,\n changesetDiscardTool,\n changesetVerifyTool,\n];\n\nexport class McpToolRegistry {\n /** Registers every model tool onto the given MCP server instance. */\n registerAll(server: McpServer): void {\n for (const tool of MODEL_TOOLS) {\n server.registerTool(\n tool.name,\n { description: tool.description, inputSchema: tool.inputSchema },\n (args: Parameters<typeof tool.handler>[0]) =>\n startActiveSpan(\n `mcp.tool.${tool.name}`,\n async (span) => {\n // Changeset-only mode (DR_CHANGESET_ONLY): refuse base-model writes up front.\n const refusal = await changesetOnlyRefusal(tool.name, (args as { rootPath?: string }).rootPath, loadModel);\n const result = refusal ? errorResult(refusal) : await tool.handler(args);\n if (isTelemetryEnabled && result?.isError) {\n const firstContent = result.content?.[0];\n const message =\n firstContent?.type === \"text\" ? firstContent.text : \"Tool returned an error\";\n span.setStatus({ code: 2, message });\n }\n return result;\n },\n { \"mcp.tool.name\": tool.name }\n )\n );\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAuBA,SAAS,oBAAoB,uBAAuB;AACpD,SACE,sBACA,sBACA,mBACA,oBACA,qBACA,sBACA,uBACA,oBACA,qBACA,yBACA,qBACA,mBACA,mBACA,gBACA,gBACA,cACA,iBACA,iBACA,eACA,eACA,iBACA,iBACA,eACA,gBACA,gBACA,iBACA,mBACA,qBACA,wBACA,oBAEK;AACP,SAAS,sBAAsB,sBAAsB,2BAA2B;AAChF,SAAS,2BAA2B;AACpC,SAAS,aAAa,iBAAiB;AACvC,SAAS,4BAA4B;AAErC,MAAM,cAAmC;EACvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI,MAAO,gBAAe;;EAE1B,YAAY,QAAiB;AAC3B,eAAW,QAAQ;AACjB,aAAO,aACL,KAAK,MACL,EAAE,aAAa,KAAK,aAAa,aAAa,KAAK,YAAW,GAC9D,CAAC,SACC,gBACE,YAAY,KAAK,IAAI,IACrB,OAAO,SAAQ;AAEb,cAAM,UAAU,MAAM,qBAAqB,KAAK,MAAO,KAA+B,UAAU,SAAS,GACnG,SAAS,UAAU,YAAY,OAAO,IAAI,MAAM,KAAK,QAAQ,IAAI;AACvE,YAAI,sBAAsB,QAAQ,SAAS;AACzC,gBAAM,eAAe,OAAO,UAAU,CAAC,GACjC,UACJ,cAAc,SAAS,SAAS,aAAa,OAAO;AACtD,eAAK,UAAU,EAAE,MAAM,GAAG,QAAO,CAAE;QACrC;AACA,eAAO;MACT,GACA,EAAE,iBAAiB,KAAK,KAAI,CAAE,CAC/B;EAGT;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|