@documentation-robotics/cli 0.1.12 → 0.2.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 +39 -1
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- package/dist/cli.js +101 -27
- 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 +122 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/commands/add.js +10 -6
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +18 -10
- 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.js +238 -145
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +45 -18
- 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 +657 -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 +107 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +53 -30
- 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 +19 -6
- package/dist/commands/update.js.map +2 -2
- 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/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/farm-manifest.js +134 -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 +159 -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 +55 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model.js +177 -16
- package/dist/core/model.js.map +3 -3
- package/dist/core/virtual-projection.js +1 -0
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/generated/node-types.js +1 -1
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +45 -9
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +55 -0
- package/dist/integrations/claude_code/docs/README.md +4 -0
- package/dist/mcp/api-key-manager.js +98 -0
- package/dist/mcp/api-key-manager.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 +44 -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-list.js +33 -0
- package/dist/mcp/tools/changeset-list.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +39 -0
- package/dist/mcp/tools/changeset-show.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 +47 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +106 -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 +88 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +41 -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 +55 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +105 -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/shared.js +103 -0
- package/dist/mcp/tools/shared.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 +2 -2
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1 -1
- package/dist/schemas/bundled/apm.json +1 -1
- package/dist/schemas/bundled/application.json +12 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +38 -2
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +4 -4
- package/dist/schemas/bundled/motivation.json +1 -1
- package/dist/schemas/bundled/navigation.json +1 -1
- package/dist/schemas/bundled/product.json +1 -1
- package/dist/schemas/bundled/security.json +1 -1
- package/dist/schemas/bundled/technology.json +1 -1
- package/dist/schemas/bundled/testing.json +1 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +45 -7
- package/dist/server/server.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 +25 -0
- package/dist/utils/ansi-suppressor.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/errors.js +69 -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 +24 -2
- 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 +5 -0
- 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 +262 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +59 -8
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +30 -2
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-B7AOQXXX.js +81 -0
- package/dist/viewer/assets/main-vdrFrG7q.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 +6 -3
- package/dist/viewer/assets/main-BRPZyc36.css +0 -1
- package/dist/viewer/assets/main-BrpLargo.js +0 -81
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: DR MCP Server
|
|
3
|
+
description: Configure an MCP client to connect to the Documentation Robotics MCP server
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Documentation Robotics MCP Server
|
|
7
|
+
|
|
8
|
+
`dr mcp` runs the CLI as a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio, exposing model tools (list/show/search/add/update/delete/validate/export/trace elements, changesets, chat, annotations) to any MCP-aware AI client — Claude Code included.
|
|
9
|
+
|
|
10
|
+
Annotation tools (`annotation_list`, `annotation_get`, `annotation_create`, `annotation_update`, `annotation_delete`, `annotation_reply`) mirror the capabilities of the REST server's `/api/annotations/*` endpoints. Unlike the REST server's in-memory, per-process annotation store, the MCP tools persist annotations to `documentation-robotics/annotations/` on disk, so they survive across MCP sessions.
|
|
11
|
+
|
|
12
|
+
## Configuring Claude Code
|
|
13
|
+
|
|
14
|
+
Add an entry to your project's `.mcp.json` (or the equivalent global MCP config):
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"documentation-robotics": {
|
|
20
|
+
"command": "dr",
|
|
21
|
+
"args": ["mcp"],
|
|
22
|
+
"env": {
|
|
23
|
+
"DR_MCP_API_KEY": "<key printed by dr mcp>"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Other MCP clients follow the same shape: a `command`/`args` pair that launches `dr mcp`, plus `DR_MCP_API_KEY` in `env`.
|
|
31
|
+
|
|
32
|
+
## Getting the API key
|
|
33
|
+
|
|
34
|
+
Run `dr mcp` once from a terminal — it generates an API key on first launch (prompting for a storage path interactively, defaulting to `~/.dr-mcp-key` otherwise) and prints it to stderr on every launch:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
$ dr mcp
|
|
38
|
+
Generated new MCP API key, stored at /home/you/.dr-mcp-key
|
|
39
|
+
MCP API key: dr-mcp-<random>
|
|
40
|
+
Documentation Robotics MCP server ready (stdio)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Copy that key into the client config's `DR_MCP_API_KEY`. The server rejects the connection if the key is missing or doesn't match.
|
|
44
|
+
|
|
45
|
+
## Rotating the key
|
|
46
|
+
|
|
47
|
+
If a key leaks or you just want a fresh one, run:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dr mcp --regenerate-key
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This overwrites the key at its existing storage path, updates `~/.dr-config.yaml`, and prints the new key — no manual deletion of files required. Update `DR_MCP_API_KEY` in every client config after rotating.
|
|
54
|
+
|
|
55
|
+
See [cli/README.md](../../../cli/README.md#mcp-server) for the full command reference.
|
|
@@ -69,6 +69,10 @@ The comprehensive manual walkthrough covering every step from fresh project thro
|
|
|
69
69
|
|
|
70
70
|
- **[dr-init.md](../commands/dr-init.md)** — Initialize new models
|
|
71
71
|
|
|
72
|
+
### MCP Integration
|
|
73
|
+
|
|
74
|
+
- **[MCP_SERVER.md](MCP_SERVER.md)** — Configure an MCP client (Claude Code or otherwise) to connect to `dr mcp`, including the API key setup and rotation
|
|
75
|
+
|
|
72
76
|
### Skills & Agents
|
|
73
77
|
|
|
74
78
|
- **[dr-codebase-memory/SKILL.md](../skills/dr_codebase_memory/SKILL.md)** — Codebase memory and context management
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
/** Write a key to disk with owner-only permissions. */
|
|
33
|
+
async store(path, key) {
|
|
34
|
+
await mkdir(dirname(path), { recursive: !0 }), await writeFile(path, `${key}
|
|
35
|
+
`, { mode: KEY_FILE_MODE }), await chmod(path, KEY_FILE_MODE);
|
|
36
|
+
}
|
|
37
|
+
/** Constant-time comparison of a client-provided key against the expected key. */
|
|
38
|
+
validate(provided, expected) {
|
|
39
|
+
if (!provided)
|
|
40
|
+
return !1;
|
|
41
|
+
const providedBuf = Buffer.from(provided), expectedBuf = Buffer.from(expected);
|
|
42
|
+
return providedBuf.length !== expectedBuf.length ? !1 : timingSafeEqual(providedBuf, expectedBuf);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load the DR config, refusing to proceed if the file exists but is
|
|
46
|
+
* corrupted. Without this, a corrupted config looks identical to "no key
|
|
47
|
+
* configured yet", which would cause `ensureKey`/`rotate` to silently
|
|
48
|
+
* generate a new key and overwrite `mcp.api_key_path` — orphaning any
|
|
49
|
+
* valid key already on disk and breaking every MCP client configured with
|
|
50
|
+
* the old one.
|
|
51
|
+
*/
|
|
52
|
+
async loadConfigOrThrow() {
|
|
53
|
+
try {
|
|
54
|
+
return await loadDRConfig({ strict: !0 });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
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;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the API key to use for this session: load it from the previously
|
|
61
|
+
* configured path, or generate+store a new one on first use.
|
|
62
|
+
*
|
|
63
|
+
* @param promptForPath Called only when no key is yet configured, to ask the
|
|
64
|
+
* user where to store it. When omitted (e.g. non-interactive launches),
|
|
65
|
+
* `defaultKeyPath` is used without prompting.
|
|
66
|
+
*/
|
|
67
|
+
async ensureKey(promptForPath) {
|
|
68
|
+
const config = await this.loadConfigOrThrow(), existingPath = config.mcp?.api_key_path;
|
|
69
|
+
if (existingPath && existsSync(existingPath))
|
|
70
|
+
return { key: await this.load(existingPath), path: existingPath, isNew: !1 };
|
|
71
|
+
const chosenPath = promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath, key = this.generate();
|
|
72
|
+
return await this.store(chosenPath, key), await saveDRConfig({
|
|
73
|
+
...config,
|
|
74
|
+
mcp: { ...config.mcp, api_key_path: chosenPath }
|
|
75
|
+
}), { key, path: chosenPath, isNew: !0 };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Force-generate a new API key, overwriting whatever is at the configured
|
|
79
|
+
* (or default) storage path, regardless of whether a key already exists
|
|
80
|
+
* there. Unlike `ensureKey`, this always writes a fresh key.
|
|
81
|
+
*
|
|
82
|
+
* @param promptForPath Called only when no storage path is yet configured,
|
|
83
|
+
* to ask the user where to store it. When omitted, `defaultKeyPath` is
|
|
84
|
+
* used without prompting.
|
|
85
|
+
*/
|
|
86
|
+
async rotate(promptForPath) {
|
|
87
|
+
const config = await this.loadConfigOrThrow(), path = config.mcp?.api_key_path ?? (promptForPath ? await promptForPath(this.defaultKeyPath) : this.defaultKeyPath), key = this.generate();
|
|
88
|
+
return await this.store(path, key), await saveDRConfig({
|
|
89
|
+
...config,
|
|
90
|
+
mcp: { ...config.mcp, api_key_path: path }
|
|
91
|
+
}), { key, path };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
ApiKeyManager,
|
|
96
|
+
getDefaultApiKeyPath
|
|
97
|
+
};
|
|
98
|
+
//# 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 /** 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 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;;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,gBAEH,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,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,44 @@
|
|
|
1
|
+
import { isTelemetryEnabled, startActiveSpan } from "../telemetry/index.js";
|
|
2
|
+
import { annotationCreateTool, annotationDeleteTool, annotationGetTool, annotationListTool, annotationReplyTool, annotationUpdateTool, changesetListTool, changesetShowTool, chatLaunchTool, chatStatusTool, modelAddTool, modelDeleteTool, modelExportTool, modelInfoTool, modelListTool, modelReloadTool, modelSearchTool, modelShowTool, modelStatsTool, modelTraceTool, modelUpdateTool, modelValidateTool } from "./tools/index.js";
|
|
3
|
+
const MODEL_TOOLS = [
|
|
4
|
+
modelListTool,
|
|
5
|
+
modelShowTool,
|
|
6
|
+
modelSearchTool,
|
|
7
|
+
modelStatsTool,
|
|
8
|
+
modelInfoTool,
|
|
9
|
+
modelTraceTool,
|
|
10
|
+
modelAddTool,
|
|
11
|
+
modelUpdateTool,
|
|
12
|
+
modelDeleteTool,
|
|
13
|
+
modelValidateTool,
|
|
14
|
+
modelExportTool,
|
|
15
|
+
modelReloadTool,
|
|
16
|
+
changesetListTool,
|
|
17
|
+
changesetShowTool,
|
|
18
|
+
chatStatusTool,
|
|
19
|
+
chatLaunchTool,
|
|
20
|
+
annotationListTool,
|
|
21
|
+
annotationGetTool,
|
|
22
|
+
annotationCreateTool,
|
|
23
|
+
annotationUpdateTool,
|
|
24
|
+
annotationDeleteTool,
|
|
25
|
+
annotationReplyTool
|
|
26
|
+
];
|
|
27
|
+
class McpToolRegistry {
|
|
28
|
+
/** Registers every model tool onto the given MCP server instance. */
|
|
29
|
+
registerAll(server) {
|
|
30
|
+
for (const tool of MODEL_TOOLS)
|
|
31
|
+
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args) => startActiveSpan(`mcp.tool.${tool.name}`, async (span) => {
|
|
32
|
+
const result = await tool.handler(args);
|
|
33
|
+
if (isTelemetryEnabled && result?.isError) {
|
|
34
|
+
const firstContent = result.content?.[0], message = firstContent?.type === "text" ? firstContent.text : "Tool returned an error";
|
|
35
|
+
span.setStatus({ code: 2, message });
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}, { "mcp.tool.name": tool.name }));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
McpToolRegistry
|
|
43
|
+
};
|
|
44
|
+
//# 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 * 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 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 type McpToolDefinition,\n} from \"./tools/index.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 chatStatusTool,\n chatLaunchTool,\n annotationListTool,\n annotationGetTool,\n annotationCreateTool,\n annotationUpdateTool,\n annotationDeleteTool,\n annotationReplyTool,\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 const result = 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": "AAiBA,SAAS,oBAAoB,uBAAuB;AACpD,SACE,sBACA,sBACA,mBACA,oBACA,qBACA,sBACA,mBACA,mBACA,gBACA,gBACA,cACA,iBACA,iBACA,eACA,eACA,iBACA,iBACA,eACA,gBACA,gBACA,iBACA,yBAEK;AAEP,MAAM,cAAmC;EACvC;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;AACb,cAAM,SAAS,MAAM,KAAK,QAAQ,IAAI;AACtC,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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AnnotationStore } from "../../core/annotation-store.js";
|
|
2
|
+
import { findElementLayer } from "../../utils/element-utils.js";
|
|
3
|
+
import { CLIError, ErrorCategory } from "../../utils/errors.js";
|
|
4
|
+
import { annotationAuthorSchema, annotationContentSchema, annotationTagsSchema, elementIdSchema } from "./annotation-shared.js";
|
|
5
|
+
import { jsonResult, loadModel, rootPathSchema, runTool } from "./shared.js";
|
|
6
|
+
const inputSchema = {
|
|
7
|
+
elementId: elementIdSchema,
|
|
8
|
+
content: annotationContentSchema,
|
|
9
|
+
author: annotationAuthorSchema,
|
|
10
|
+
tags: annotationTagsSchema,
|
|
11
|
+
rootPath: rootPathSchema
|
|
12
|
+
};
|
|
13
|
+
async function annotationCreateHandler(args) {
|
|
14
|
+
return runTool(async () => {
|
|
15
|
+
const model = await loadModel(args.rootPath);
|
|
16
|
+
if (!await findElementLayer(model, args.elementId))
|
|
17
|
+
throw new CLIError(`Element ${args.elementId} not found`, ErrorCategory.NOT_FOUND, [
|
|
18
|
+
'Use "model_search" to find similar elements'
|
|
19
|
+
]);
|
|
20
|
+
const annotation = await new AnnotationStore(model.rootPath).create({
|
|
21
|
+
elementId: args.elementId,
|
|
22
|
+
content: args.content,
|
|
23
|
+
author: args.author,
|
|
24
|
+
tags: args.tags
|
|
25
|
+
});
|
|
26
|
+
return jsonResult(annotation);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const annotationCreateTool = {
|
|
30
|
+
name: "annotation_create",
|
|
31
|
+
description: "Create a new annotation on a model element.",
|
|
32
|
+
inputSchema,
|
|
33
|
+
handler: annotationCreateHandler
|
|
34
|
+
};
|
|
35
|
+
export {
|
|
36
|
+
annotationCreateHandler,
|
|
37
|
+
annotationCreateTool
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=annotation-create.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/mcp/tools/annotation-create.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * annotation_create \u2014 create a new annotation on a model element. Mirrors\n * `POST /api/annotations` on the REST server.\n */\n\nimport type { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { AnnotationStore } from \"../../core/annotation-store.js\";\nimport { findElementLayer } from \"../../utils/element-utils.js\";\nimport { CLIError, ErrorCategory } from \"../../utils/errors.js\";\nimport {\n annotationAuthorSchema,\n annotationContentSchema,\n annotationTagsSchema,\n elementIdSchema,\n} from \"./annotation-shared.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\n\nconst inputSchema = {\n elementId: elementIdSchema,\n content: annotationContentSchema,\n author: annotationAuthorSchema,\n tags: annotationTagsSchema,\n rootPath: rootPathSchema,\n};\n\nexport type AnnotationCreateArgs = z.infer<z.ZodObject<typeof inputSchema>>;\n\nexport async function annotationCreateHandler(args: AnnotationCreateArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n\n const layerName = await findElementLayer(model, args.elementId);\n if (!layerName) {\n throw new CLIError(`Element ${args.elementId} not found`, ErrorCategory.NOT_FOUND, [\n 'Use \"model_search\" to find similar elements',\n ]);\n }\n\n const store = new AnnotationStore(model.rootPath);\n const annotation = await store.create({\n elementId: args.elementId,\n content: args.content,\n author: args.author,\n tags: args.tags,\n });\n\n return jsonResult(annotation);\n });\n}\n\nexport const annotationCreateTool: McpToolDefinition<typeof inputSchema> = {\n name: \"annotation_create\",\n description: \"Create a new annotation on a model element.\",\n inputSchema,\n handler: annotationCreateHandler,\n};\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,UAAU,qBAAqB;AACxC,SACE,wBACA,yBACA,sBACA,uBACK;AACP,SAAS,YAAY,WAAW,gBAAgB,eAAuC;AAEvF,MAAM,cAAc;EAClB,WAAW;EACX,SAAS;EACT,QAAQ;EACR,MAAM;EACN,UAAU;;AAKZ,eAAsB,wBAAwB,MAA0B;AACtE,SAAO,QAAQ,YAAW;AACxB,UAAM,QAAQ,MAAM,UAAU,KAAK,QAAQ;AAG3C,QAAI,CADc,MAAM,iBAAiB,OAAO,KAAK,SAAS;AAE5D,YAAM,IAAI,SAAS,WAAW,KAAK,SAAS,cAAc,cAAc,WAAW;QACjF;OACD;AAIH,UAAM,aAAa,MADL,IAAI,gBAAgB,MAAM,QAAQ,EACjB,OAAO;MACpC,WAAW,KAAK;MAChB,SAAS,KAAK;MACd,QAAQ,KAAK;MACb,MAAM,KAAK;KACZ;AAED,WAAO,WAAW,UAAU;EAC9B,CAAC;AACH;AAEO,MAAM,uBAA8D;EACzE,MAAM;EACN,aAAa;EACb;EACA,SAAS;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|