callman-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/client/api.d.ts +109 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +180 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/errors.d.ts +27 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +98 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/http.d.ts +34 -0
- package/dist/client/http.d.ts.map +1 -0
- package/dist/client/http.js +121 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/types.d.ts +327 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +5 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +35 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +91 -0
- package/dist/context.js.map +1 -0
- package/dist/domain/diagram/build.d.ts +260 -0
- package/dist/domain/diagram/build.d.ts.map +1 -0
- package/dist/domain/diagram/build.js +160 -0
- package/dist/domain/diagram/build.js.map +1 -0
- package/dist/domain/diagram/layout.d.ts +17 -0
- package/dist/domain/diagram/layout.d.ts.map +1 -0
- package/dist/domain/diagram/layout.js +61 -0
- package/dist/domain/diagram/layout.js.map +1 -0
- package/dist/domain/diagram/patch.d.ts +330 -0
- package/dist/domain/diagram/patch.d.ts.map +1 -0
- package/dist/domain/diagram/patch.js +176 -0
- package/dist/domain/diagram/patch.js.map +1 -0
- package/dist/domain/diagram/summarize.d.ts +41 -0
- package/dist/domain/diagram/summarize.d.ts.map +1 -0
- package/dist/domain/diagram/summarize.js +47 -0
- package/dist/domain/diagram/summarize.js.map +1 -0
- package/dist/domain/importDetect.d.ts +6 -0
- package/dist/domain/importDetect.d.ts.map +1 -0
- package/dist/domain/importDetect.js +32 -0
- package/dist/domain/importDetect.js.map +1 -0
- package/dist/domain/requestShape.d.ts +796 -0
- package/dist/domain/requestShape.d.ts.map +1 -0
- package/dist/domain/requestShape.js +250 -0
- package/dist/domain/requestShape.js.map +1 -0
- package/dist/domain/resolve.d.ts +30 -0
- package/dist/domain/resolve.d.ts.map +1 -0
- package/dist/domain/resolve.js +56 -0
- package/dist/domain/resolve.js.map +1 -0
- package/dist/domain/scenario/build.d.ts +270 -0
- package/dist/domain/scenario/build.d.ts.map +1 -0
- package/dist/domain/scenario/build.js +135 -0
- package/dist/domain/scenario/build.js.map +1 -0
- package/dist/domain/scenario/summarize.d.ts +206 -0
- package/dist/domain/scenario/summarize.d.ts.map +1 -0
- package/dist/domain/scenario/summarize.js +138 -0
- package/dist/domain/scenario/summarize.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cache.d.ts +12 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/cache.js +40 -0
- package/dist/lib/cache.js.map +1 -0
- package/dist/lib/ids.d.ts +5 -0
- package/dist/lib/ids.d.ts.map +1 -0
- package/dist/lib/ids.js +19 -0
- package/dist/lib/ids.js.map +1 -0
- package/dist/lib/log.d.ts +10 -0
- package/dist/lib/log.d.ts.map +1 -0
- package/dist/lib/log.js +25 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/mask.d.ts +24 -0
- package/dist/lib/mask.d.ts.map +1 -0
- package/dist/lib/mask.js +63 -0
- package/dist/lib/mask.js.map +1 -0
- package/dist/lib/pagination.d.ts +21 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +40 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/truncate.d.ts +25 -0
- package/dist/lib/truncate.d.ts.map +1 -0
- package/dist/lib/truncate.js +45 -0
- package/dist/lib/truncate.js.map +1 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -0
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +72 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/docs/diagramSchema.d.ts +2 -0
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -0
- package/dist/resources/docs/diagramSchema.js +65 -0
- package/dist/resources/docs/diagramSchema.js.map +1 -0
- package/dist/resources/docs/overview.d.ts +2 -0
- package/dist/resources/docs/overview.d.ts.map +1 -0
- package/dist/resources/docs/overview.js +46 -0
- package/dist/resources/docs/overview.js.map +1 -0
- package/dist/resources/docs/requestSchema.d.ts +2 -0
- package/dist/resources/docs/requestSchema.d.ts.map +1 -0
- package/dist/resources/docs/requestSchema.js +70 -0
- package/dist/resources/docs/requestSchema.js.map +1 -0
- package/dist/resources/generated/aiPacks.d.ts +3 -0
- package/dist/resources/generated/aiPacks.d.ts.map +1 -0
- package/dist/resources/generated/aiPacks.js +11 -0
- package/dist/resources/generated/aiPacks.js.map +1 -0
- package/dist/resources/index.d.ts +4 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +114 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/run/executeCollection.d.ts +19 -0
- package/dist/run/executeCollection.d.ts.map +1 -0
- package/dist/run/executeCollection.js +89 -0
- package/dist/run/executeCollection.js.map +1 -0
- package/dist/run/loadBundle.d.ts +9 -0
- package/dist/run/loadBundle.d.ts.map +1 -0
- package/dist/run/loadBundle.js +242 -0
- package/dist/run/loadBundle.js.map +1 -0
- package/dist/run/resolveTemplate.d.ts +2 -0
- package/dist/run/resolveTemplate.d.ts.map +1 -0
- package/dist/run/resolveTemplate.js +12 -0
- package/dist/run/resolveTemplate.js.map +1 -0
- package/dist/run/summarize.d.ts +73 -0
- package/dist/run/summarize.d.ts.map +1 -0
- package/dist/run/summarize.js +69 -0
- package/dist/run/summarize.js.map +1 -0
- package/dist/run/types.d.ts +46 -0
- package/dist/run/types.d.ts.map +1 -0
- package/dist/run/types.js +2 -0
- package/dist/run/types.js.map +1 -0
- package/dist/server.d.ts +23 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/catalog.d.ts +18 -0
- package/dist/tools/catalog.d.ts.map +1 -0
- package/dist/tools/catalog.js +66 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/collections.d.ts +4 -0
- package/dist/tools/collections.d.ts.map +1 -0
- package/dist/tools/collections.js +201 -0
- package/dist/tools/collections.js.map +1 -0
- package/dist/tools/diagrams.d.ts +4 -0
- package/dist/tools/diagrams.d.ts.map +1 -0
- package/dist/tools/diagrams.js +226 -0
- package/dist/tools/diagrams.js.map +1 -0
- package/dist/tools/environments.d.ts +4 -0
- package/dist/tools/environments.d.ts.map +1 -0
- package/dist/tools/environments.js +150 -0
- package/dist/tools/environments.js.map +1 -0
- package/dist/tools/import.d.ts +6 -0
- package/dist/tools/import.d.ts.map +1 -0
- package/dist/tools/import.js +144 -0
- package/dist/tools/import.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +26 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/lookups.d.ts +33 -0
- package/dist/tools/lookups.d.ts.map +1 -0
- package/dist/tools/lookups.js +132 -0
- package/dist/tools/lookups.js.map +1 -0
- package/dist/tools/meta.d.ts +4 -0
- package/dist/tools/meta.d.ts.map +1 -0
- package/dist/tools/meta.js +78 -0
- package/dist/tools/meta.js.map +1 -0
- package/dist/tools/requests.d.ts +4 -0
- package/dist/tools/requests.d.ts.map +1 -0
- package/dist/tools/requests.js +247 -0
- package/dist/tools/requests.js.map +1 -0
- package/dist/tools/run.d.ts +4 -0
- package/dist/tools/run.d.ts.map +1 -0
- package/dist/tools/run.js +167 -0
- package/dist/tools/run.js.map +1 -0
- package/dist/tools/scenarios.d.ts +4 -0
- package/dist/tools/scenarios.d.ts.map +1 -0
- package/dist/tools/scenarios.js +242 -0
- package/dist/tools/scenarios.js.map +1 -0
- package/dist/tools/shared.d.ts +20 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +74 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/workspaces.d.ts +4 -0
- package/dist/tools/workspaces.d.ts.map +1 -0
- package/dist/tools/workspaces.js +80 -0
- package/dist/tools/workspaces.js.map +1 -0
- package/dist-cjs/client/api.js +183 -0
- package/dist-cjs/client/errors.js +104 -0
- package/dist-cjs/client/http.js +124 -0
- package/dist-cjs/client/types.js +5 -0
- package/dist-cjs/config.js +73 -0
- package/dist-cjs/context.js +94 -0
- package/dist-cjs/domain/diagram/build.js +164 -0
- package/dist-cjs/domain/diagram/layout.js +98 -0
- package/dist-cjs/domain/diagram/patch.js +179 -0
- package/dist-cjs/domain/diagram/summarize.js +52 -0
- package/dist-cjs/domain/importDetect.js +40 -0
- package/dist-cjs/domain/requestShape.js +260 -0
- package/dist-cjs/domain/resolve.js +63 -0
- package/dist-cjs/domain/scenario/build.js +140 -0
- package/dist-cjs/domain/scenario/summarize.js +144 -0
- package/dist-cjs/lib/cache.js +43 -0
- package/dist-cjs/lib/ids.js +24 -0
- package/dist-cjs/lib/log.js +28 -0
- package/dist-cjs/lib/mask.js +72 -0
- package/dist-cjs/lib/pagination.js +48 -0
- package/dist-cjs/lib/truncate.js +52 -0
- package/dist-cjs/main.js +31 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/prompts/index.js +75 -0
- package/dist-cjs/resources/docs/diagramSchema.js +67 -0
- package/dist-cjs/resources/docs/overview.js +48 -0
- package/dist-cjs/resources/docs/requestSchema.js +72 -0
- package/dist-cjs/resources/generated/aiPacks.js +13 -0
- package/dist-cjs/resources/index.js +117 -0
- package/dist-cjs/run/executeCollection.js +93 -0
- package/dist-cjs/run/loadBundle.js +246 -0
- package/dist-cjs/run/resolveTemplate.js +15 -0
- package/dist-cjs/run/summarize.js +73 -0
- package/dist-cjs/run/types.js +2 -0
- package/dist-cjs/server.js +28 -0
- package/dist-cjs/tools/catalog.js +69 -0
- package/dist-cjs/tools/collections.js +204 -0
- package/dist-cjs/tools/diagrams.js +229 -0
- package/dist-cjs/tools/environments.js +153 -0
- package/dist-cjs/tools/import.js +147 -0
- package/dist-cjs/tools/index.js +31 -0
- package/dist-cjs/tools/lookups.js +149 -0
- package/dist-cjs/tools/meta.js +81 -0
- package/dist-cjs/tools/requests.js +250 -0
- package/dist-cjs/tools/run.js +203 -0
- package/dist-cjs/tools/scenarios.js +245 -0
- package/dist-cjs/tools/shared.js +81 -0
- package/dist-cjs/tools/workspaces.js +83 -0
- package/package.json +80 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeRun = exports.summarizeResult = void 0;
|
|
4
|
+
const mask_js_1 = require("../lib/mask.js");
|
|
5
|
+
const truncate_js_1 = require("../lib/truncate.js");
|
|
6
|
+
const executeCollection_js_1 = require("./executeCollection.js");
|
|
7
|
+
// What a local run reports back to the model: enough to judge and debug
|
|
8
|
+
// (status, timing, tests, a bounded body preview, the request as SENT with
|
|
9
|
+
// sensitive headers masked) and nothing that leaks credentials — variable
|
|
10
|
+
// VALUES never appear, only the keys scripts changed.
|
|
11
|
+
const KEEP_RESPONSE_HEADERS = new Set([
|
|
12
|
+
"content-type",
|
|
13
|
+
"content-length",
|
|
14
|
+
"location",
|
|
15
|
+
"cache-control",
|
|
16
|
+
"x-request-id",
|
|
17
|
+
"x-correlation-id",
|
|
18
|
+
"retry-after",
|
|
19
|
+
"www-authenticate",
|
|
20
|
+
]);
|
|
21
|
+
const responseHeadersOf = (response) => {
|
|
22
|
+
const headers = response?.headers;
|
|
23
|
+
if (!headers || typeof headers !== "object")
|
|
24
|
+
return {};
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
27
|
+
if (KEEP_RESPONSE_HEADERS.has(key.toLowerCase()))
|
|
28
|
+
out[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
const bodyOf = (response) => {
|
|
33
|
+
if (!response || typeof response !== "object")
|
|
34
|
+
return null;
|
|
35
|
+
const record = response;
|
|
36
|
+
return record.data !== undefined ? record.data : record.body !== undefined ? record.body : null;
|
|
37
|
+
};
|
|
38
|
+
const summarizeResult = (result, maxBodyBytes) => {
|
|
39
|
+
const body = bodyOf(result.response);
|
|
40
|
+
const bodyText = body === null || body === undefined ? "" : typeof body === "string" ? body : (0, truncate_js_1.safeStringify)(body);
|
|
41
|
+
const clipped = (0, truncate_js_1.clipBytes)(bodyText, maxBodyBytes);
|
|
42
|
+
const sent = result.request;
|
|
43
|
+
return {
|
|
44
|
+
requestId: result.requestId,
|
|
45
|
+
name: result.requestName ?? result.requestId,
|
|
46
|
+
passed: !(0, executeCollection_js_1.requestFailed)(result),
|
|
47
|
+
status: result.status,
|
|
48
|
+
durationMs: result.duration,
|
|
49
|
+
request: sent
|
|
50
|
+
? {
|
|
51
|
+
method: sent.method,
|
|
52
|
+
url: (0, truncate_js_1.clip)(sent.url, 2048),
|
|
53
|
+
headers: (0, mask_js_1.maskHeaderRecord)(sent.headers),
|
|
54
|
+
...(sent.body !== undefined && sent.body !== null ? { body: (0, truncate_js_1.clip)(typeof sent.body === "string" ? sent.body : (0, truncate_js_1.safeStringify)(sent.body), truncate_js_1.LIMITS.BODY_PREVIEW) } : {}),
|
|
55
|
+
}
|
|
56
|
+
: null,
|
|
57
|
+
response: {
|
|
58
|
+
headers: responseHeadersOf(result.response),
|
|
59
|
+
body: clipped.text || null,
|
|
60
|
+
...(clipped.truncated ? { bodyTruncated: true, bodyBytes: clipped.totalBytes } : {}),
|
|
61
|
+
},
|
|
62
|
+
tests: result.scriptResults.map((test) => ({ name: test.name, status: test.status, ...(test.message ? { message: (0, truncate_js_1.clip)(test.message, 300) } : {}) })),
|
|
63
|
+
contracts: result.contractResults.map((test) => ({ name: test.name, status: test.status, ...(test.message ? { message: (0, truncate_js_1.clip)(test.message, 300) } : {}) })),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
exports.summarizeResult = summarizeResult;
|
|
67
|
+
const summarizeRun = (outcome, maxBodyBytes, extra = {}) => ({
|
|
68
|
+
...extra,
|
|
69
|
+
summary: { ...outcome.summary, bailed: outcome.bailed },
|
|
70
|
+
results: outcome.results.map((result) => (0, exports.summarizeResult)(result, maxBodyBytes)),
|
|
71
|
+
env: outcome.envChanges,
|
|
72
|
+
});
|
|
73
|
+
exports.summarizeRun = summarizeRun;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCallmanMcpServer = exports.SERVER_INSTRUCTIONS = exports.SERVER_NAME = void 0;
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const context_js_1 = require("./context.js");
|
|
6
|
+
const index_js_1 = require("./prompts/index.js");
|
|
7
|
+
const index_js_2 = require("./resources/index.js");
|
|
8
|
+
const index_js_3 = require("./tools/index.js");
|
|
9
|
+
exports.SERVER_NAME = "callman";
|
|
10
|
+
exports.SERVER_INSTRUCTIONS = `Callman is an API client and test platform: collections of HTTP requests, environments with variables, multi-step test scenarios, and system-design diagrams. You act as the user whose token is configured, within that token's permissions and the user's workspace role.
|
|
11
|
+
Start with whoami. Names work as references (collection, folder, request, environment, scenario, diagram, workspace); ambiguous names return candidates. List before creating — duplicate collection names are refused. Preview large writes with dryRun: true; deletions need confirm: true and the user's agreement. Environment values are masked unless the token holds environments:reveal and you pass reveal: true; never write a masked value back. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating requests, callman://docs/scenario-nodes before scenarios, and callman://diagram/components before diagrams.`;
|
|
12
|
+
const createCallmanMcpServer = (options) => {
|
|
13
|
+
const version = options.version ?? "0.0.0";
|
|
14
|
+
const context = (0, context_js_1.createToolContext)({
|
|
15
|
+
config: options.config,
|
|
16
|
+
version,
|
|
17
|
+
fetch: options.fetch,
|
|
18
|
+
logger: options.logger,
|
|
19
|
+
cacheTtlMs: options.cacheTtlMs,
|
|
20
|
+
pollDelaysMs: options.pollDelaysMs,
|
|
21
|
+
});
|
|
22
|
+
const server = new mcp_js_1.McpServer({ name: exports.SERVER_NAME, version, title: "Callman" }, { capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: exports.SERVER_INSTRUCTIONS });
|
|
23
|
+
(0, index_js_3.registerAllTools)(server, context);
|
|
24
|
+
(0, index_js_2.registerResources)(server, context);
|
|
25
|
+
(0, index_js_1.registerPrompts)(server, context);
|
|
26
|
+
return { server, context };
|
|
27
|
+
};
|
|
28
|
+
exports.createCallmanMcpServer = createCallmanMcpServer;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The tool ↔ scope catalog: what a token needs for each tool. Used by
|
|
3
|
+
// `whoami` to tell the model up front which tools will work with the current
|
|
4
|
+
// token, by the README/in-app guide (kept in sync by hand), and by the
|
|
5
|
+
// backend host later to advertise the same list.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.toolAvailability = exports.TOOL_CATALOG = void 0;
|
|
8
|
+
exports.TOOL_CATALOG = [
|
|
9
|
+
{ name: "whoami", group: "meta", scopes: [] },
|
|
10
|
+
{ name: "list_workspaces", group: "workspaces", scopes: ["workspaces:read"] },
|
|
11
|
+
{ name: "list_workspace_members", group: "workspaces", scopes: ["workspaces:read"] },
|
|
12
|
+
{ name: "add_workspace_member", group: "workspaces", scopes: ["members:manage"] },
|
|
13
|
+
{ name: "remove_workspace_member", group: "workspaces", scopes: ["members:manage"], destructive: true },
|
|
14
|
+
{ name: "list_collections", group: "collections", scopes: ["collections:read"] },
|
|
15
|
+
{ name: "get_collection_tree", group: "collections", scopes: ["collections:read", "requests:read"] },
|
|
16
|
+
{ name: "export_collection", group: "collections", scopes: ["collections:read"] },
|
|
17
|
+
{ name: "create_collection", group: "collections", scopes: ["collections:write"] },
|
|
18
|
+
{ name: "update_collection", group: "collections", scopes: ["collections:write"] },
|
|
19
|
+
{ name: "create_folder", group: "collections", scopes: ["collections:write"] },
|
|
20
|
+
{ name: "update_folder", group: "collections", scopes: ["collections:write"] },
|
|
21
|
+
{ name: "delete_collection", group: "collections", scopes: ["collections:delete"], destructive: true },
|
|
22
|
+
{ name: "delete_folder", group: "collections", scopes: ["collections:delete"], destructive: true },
|
|
23
|
+
{ name: "search_requests", group: "requests", scopes: ["requests:read"] },
|
|
24
|
+
{ name: "get_request", group: "requests", scopes: ["requests:read"] },
|
|
25
|
+
{ name: "create_request", group: "requests", scopes: ["requests:write"] },
|
|
26
|
+
// Existing collection → POST /requests (requests:write); new collection → the
|
|
27
|
+
// atomic POST /import/json (import:write). Either makes the tool usable.
|
|
28
|
+
{ name: "create_requests_bulk", group: "requests", scopes: ["requests:write", "import:write"], anyOf: true },
|
|
29
|
+
{ name: "update_request", group: "requests", scopes: ["requests:write"] },
|
|
30
|
+
{ name: "move_requests", group: "requests", scopes: ["requests:write"] },
|
|
31
|
+
{ name: "delete_requests", group: "requests", scopes: ["requests:delete"], destructive: true },
|
|
32
|
+
{ name: "run_request", group: "run", scopes: ["requests:run"], localRun: true },
|
|
33
|
+
{ name: "run_collection", group: "run", scopes: ["requests:run"], localRun: true },
|
|
34
|
+
// Documents and new collections → import:write; cURL/.http into an existing
|
|
35
|
+
// collection → requests:write.
|
|
36
|
+
{ name: "import_content", group: "import", scopes: ["import:write", "requests:write"], anyOf: true },
|
|
37
|
+
{ name: "import_scenario", group: "import", scopes: ["import:write"] },
|
|
38
|
+
{ name: "list_environments", group: "environments", scopes: ["environments:read"] },
|
|
39
|
+
{ name: "get_environment", group: "environments", scopes: ["environments:read"] },
|
|
40
|
+
{ name: "create_environment", group: "environments", scopes: ["environments:write"] },
|
|
41
|
+
{ name: "set_environment_variables", group: "environments", scopes: ["environments:write"] },
|
|
42
|
+
{ name: "delete_environment", group: "environments", scopes: ["environments:delete"], destructive: true },
|
|
43
|
+
{ name: "list_scenarios", group: "scenarios", scopes: ["scenarios:read"] },
|
|
44
|
+
{ name: "get_scenario", group: "scenarios", scopes: ["scenarios:read"] },
|
|
45
|
+
{ name: "get_scenario_report", group: "scenarios", scopes: ["scenarios:read"] },
|
|
46
|
+
{ name: "create_scenario", group: "scenarios", scopes: ["scenarios:write"] },
|
|
47
|
+
{ name: "update_scenario", group: "scenarios", scopes: ["scenarios:write"] },
|
|
48
|
+
{ name: "delete_scenario", group: "scenarios", scopes: ["scenarios:delete"], destructive: true },
|
|
49
|
+
{ name: "run_scenario", group: "scenarios", scopes: ["scenarios:run"] },
|
|
50
|
+
{ name: "list_diagrams", group: "diagrams", scopes: ["diagrams:read"] },
|
|
51
|
+
{ name: "get_diagram", group: "diagrams", scopes: ["diagrams:read"] },
|
|
52
|
+
{ name: "create_diagram", group: "diagrams", scopes: ["diagrams:write"] },
|
|
53
|
+
{ name: "update_diagram", group: "diagrams", scopes: ["diagrams:write"] },
|
|
54
|
+
{ name: "patch_diagram", group: "diagrams", scopes: ["diagrams:write"] },
|
|
55
|
+
{ name: "delete_diagram", group: "diagrams", scopes: ["diagrams:delete"], destructive: true },
|
|
56
|
+
{ name: "share_diagram", group: "diagrams", scopes: ["diagrams:share"] },
|
|
57
|
+
];
|
|
58
|
+
const toolAvailability = (tokenScopes, allowLocalRun) => {
|
|
59
|
+
const held = new Set(tokenScopes);
|
|
60
|
+
return exports.TOOL_CATALOG.map((entry) => {
|
|
61
|
+
if (entry.localRun && !allowLocalRun) {
|
|
62
|
+
return { name: entry.name, available: false, missingScopes: [], reason: "local runs are disabled (CALLMAN_ALLOW_RUN=0)" };
|
|
63
|
+
}
|
|
64
|
+
const missing = entry.scopes.filter((scope) => !held.has(scope));
|
|
65
|
+
const available = entry.anyOf ? missing.length < entry.scopes.length : missing.length === 0;
|
|
66
|
+
return { name: entry.name, available, missingScopes: available ? [] : missing };
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
exports.toolAvailability = toolAvailability;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCollectionTools = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const errors_js_1 = require("../client/errors.js");
|
|
6
|
+
const requestShape_js_1 = require("../domain/requestShape.js");
|
|
7
|
+
const resolve_js_1 = require("../domain/resolve.js");
|
|
8
|
+
const pagination_js_1 = require("../lib/pagination.js");
|
|
9
|
+
const truncate_js_1 = require("../lib/truncate.js");
|
|
10
|
+
const lookups_js_1 = require("./lookups.js");
|
|
11
|
+
const shared_js_1 = require("./shared.js");
|
|
12
|
+
const EXPORT_MAX_BYTES = 1024 * 1024;
|
|
13
|
+
const registerCollectionTools = (server, ctx) => {
|
|
14
|
+
server.registerTool("list_collections", {
|
|
15
|
+
title: "List collections",
|
|
16
|
+
description: "List the collections in a workspace with request/folder counts. Optional `query` filters by name (substring). Use get_collection_tree to see what is inside one.",
|
|
17
|
+
inputSchema: { workspace: shared_js_1.workspaceArg, query: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
|
|
18
|
+
annotations: shared_js_1.READ,
|
|
19
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, query, cursor, limit }) => {
|
|
20
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
21
|
+
const all = await (0, lookups_js_1.loadCollections)(ctx, ws.id);
|
|
22
|
+
const needle = query?.trim().toLowerCase();
|
|
23
|
+
const filtered = needle ? all.filter((collection) => collection.name.toLowerCase().includes(needle)) : all;
|
|
24
|
+
const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit);
|
|
25
|
+
return (0, shared_js_1.ok)({
|
|
26
|
+
workspace: { id: ws.id, name: ws.name },
|
|
27
|
+
collections: page.items.map((collection) => ({
|
|
28
|
+
id: collection.id,
|
|
29
|
+
name: collection.name,
|
|
30
|
+
requestCount: collection.requestCount ?? null,
|
|
31
|
+
folderCount: collection.folderCount ?? null,
|
|
32
|
+
updatedAt: collection.updatedAt ?? null,
|
|
33
|
+
})),
|
|
34
|
+
total: page.total,
|
|
35
|
+
nextCursor: page.nextCursor,
|
|
36
|
+
});
|
|
37
|
+
}));
|
|
38
|
+
server.registerTool("get_collection_tree", {
|
|
39
|
+
title: "Get a collection's folders and requests",
|
|
40
|
+
description: "The folders (with paths like \"Auth/Login\") and requests (id, name, method, url, folder) of one collection — the map you need before creating, moving or running requests.",
|
|
41
|
+
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection") },
|
|
42
|
+
annotations: shared_js_1.READ,
|
|
43
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection }) => {
|
|
44
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
45
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
46
|
+
const tree = await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id);
|
|
47
|
+
const truncated = tree.requests.length > truncate_js_1.LIMITS.TREE_ITEMS;
|
|
48
|
+
return (0, shared_js_1.ok)({
|
|
49
|
+
collection: { id: resolved.item.id, name: resolved.item.name },
|
|
50
|
+
resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)],
|
|
51
|
+
folders: tree.folders.map((folder) => ({ id: folder.id, name: folder.name, parentId: folder.parentId, path: folder.path })),
|
|
52
|
+
requests: tree.requests.slice(0, truncate_js_1.LIMITS.TREE_ITEMS).map(requestShape_js_1.requestSummaryView),
|
|
53
|
+
...(truncated ? { truncated: true, totalRequests: tree.requests.length } : {}),
|
|
54
|
+
});
|
|
55
|
+
}));
|
|
56
|
+
server.registerTool("export_collection", {
|
|
57
|
+
title: "Export a collection",
|
|
58
|
+
description: "Export a collection as a document: `internal` (Callman's own format, re-importable, includes scripts/contracts) or `postman` (Postman v2.1). Auth secrets in the export are NOT masked — treat the result as sensitive.",
|
|
59
|
+
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), format: zod_1.z.enum(["internal", "postman"]).default("internal") },
|
|
60
|
+
annotations: shared_js_1.READ,
|
|
61
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, format }) => {
|
|
62
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
63
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
64
|
+
const document = await ctx.api.exportCollection(ws.id, resolved.item.id, format);
|
|
65
|
+
const bytes = (0, truncate_js_1.byteLength)(document);
|
|
66
|
+
if (bytes > EXPORT_MAX_BYTES) {
|
|
67
|
+
throw new errors_js_1.ToolInputError("EXPORT_TOO_LARGE", `The export is ${bytes} bytes (limit ${EXPORT_MAX_BYTES}). Export it from the desktop app instead.`, { bytes });
|
|
68
|
+
}
|
|
69
|
+
return (0, shared_js_1.ok)({ collection: { id: resolved.item.id, name: resolved.item.name }, format, bytes, document: document });
|
|
70
|
+
}));
|
|
71
|
+
server.registerTool("create_collection", {
|
|
72
|
+
title: "Create a collection",
|
|
73
|
+
description: "Create an empty collection. Refuses a name that already exists in the workspace unless allowDuplicateName is true — list_collections first and reuse an existing one when it fits.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
workspace: shared_js_1.workspaceArg,
|
|
76
|
+
name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME),
|
|
77
|
+
allowDuplicateName: zod_1.z.boolean().optional(),
|
|
78
|
+
runnerSettings: zod_1.z.object({ database: zod_1.z.boolean().optional(), kafka: zod_1.z.boolean().optional(), redis: zod_1.z.boolean().optional() }).optional(),
|
|
79
|
+
},
|
|
80
|
+
annotations: shared_js_1.WRITE,
|
|
81
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, name, allowDuplicateName, runnerSettings }) => {
|
|
82
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
83
|
+
const existing = (await (0, lookups_js_1.loadCollections)(ctx, ws.id)).find((collection) => (0, lookups_js_1.sameName)(collection.name, name));
|
|
84
|
+
if (existing && !allowDuplicateName) {
|
|
85
|
+
throw new errors_js_1.ToolInputError("COLLECTION_NAME_EXISTS", `A collection named "${existing.name}" already exists (id ${existing.id}). Reuse it, pick another name, or pass allowDuplicateName: true.`, {
|
|
86
|
+
existing: { id: existing.id, name: existing.name },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const created = await ctx.api.createCollection(ws.id, { name: name.trim(), ...(runnerSettings ? { runnerSettings: compactSettings(runnerSettings) } : {}) });
|
|
90
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
91
|
+
return (0, shared_js_1.ok)({ collection: { id: created.id, name: created.name }, note: "Created. Add requests with create_request / create_requests_bulk / import_content." });
|
|
92
|
+
}));
|
|
93
|
+
server.registerTool("update_collection", {
|
|
94
|
+
title: "Rename a collection / change runner settings",
|
|
95
|
+
description: "Rename a collection or toggle its runner settings (database / kafka / redis steps during runs).",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
workspace: shared_js_1.workspaceArg,
|
|
98
|
+
collection: (0, shared_js_1.refArg)("Collection"),
|
|
99
|
+
name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME).optional(),
|
|
100
|
+
runnerSettings: zod_1.z.object({ database: zod_1.z.boolean().optional(), kafka: zod_1.z.boolean().optional(), redis: zod_1.z.boolean().optional() }).optional(),
|
|
101
|
+
},
|
|
102
|
+
annotations: shared_js_1.IDEMPOTENT_WRITE,
|
|
103
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, name, runnerSettings }) => {
|
|
104
|
+
if (name === undefined && runnerSettings === undefined)
|
|
105
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or runnerSettings.");
|
|
106
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
107
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
108
|
+
const updated = await ctx.api.updateCollection(ws.id, resolved.item.id, {
|
|
109
|
+
...(name !== undefined ? { name: name.trim() } : {}),
|
|
110
|
+
...(runnerSettings ? { runnerSettings: compactSettings(runnerSettings) } : {}),
|
|
111
|
+
});
|
|
112
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
113
|
+
return (0, shared_js_1.ok)({ collection: { id: updated.id, name: updated.name, runnerSettings: updated.runnerSettings ?? null }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)] });
|
|
114
|
+
}));
|
|
115
|
+
server.registerTool("create_folder", {
|
|
116
|
+
title: "Create a folder",
|
|
117
|
+
description: "Create a folder in a collection, optionally inside a parent folder (id, name or path).",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
workspace: shared_js_1.workspaceArg,
|
|
120
|
+
collection: (0, shared_js_1.refArg)("Collection"),
|
|
121
|
+
name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME),
|
|
122
|
+
parent: zod_1.z.string().max(300).optional().describe("Parent folder id, name or path; omit for top level"),
|
|
123
|
+
},
|
|
124
|
+
annotations: shared_js_1.WRITE,
|
|
125
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, name, parent }) => {
|
|
126
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
127
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
128
|
+
const parentFolder = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, parent);
|
|
129
|
+
const tree = await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id);
|
|
130
|
+
const sibling = tree.folders.find((folder) => folder.parentId === (parentFolder?.item.id ?? null) && (0, lookups_js_1.sameName)(folder.name, name));
|
|
131
|
+
if (sibling) {
|
|
132
|
+
return (0, shared_js_1.ok)({ folder: { id: sibling.id, name: sibling.name, path: sibling.path }, existed: true, note: "A folder with that name already exists there — returned instead of creating a duplicate." });
|
|
133
|
+
}
|
|
134
|
+
const folder = await ctx.api.createFolder(ws.id, { name: name.trim(), collectionId: resolved.item.id, parentId: parentFolder?.item.id ?? null });
|
|
135
|
+
ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
|
|
136
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
137
|
+
return (0, shared_js_1.ok)({
|
|
138
|
+
folder: { id: folder.id, name: folder.name, parentId: folder.parentId, path: parentFolder ? `${parentFolder.item.path}/${folder.name}` : folder.name },
|
|
139
|
+
resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(parentFolder ? [(0, resolve_js_1.resolvedSummary)(parent, parentFolder)] : [])],
|
|
140
|
+
});
|
|
141
|
+
}));
|
|
142
|
+
server.registerTool("update_folder", {
|
|
143
|
+
title: "Rename or move a folder",
|
|
144
|
+
description: "Rename a folder and/or move it under another parent (pass parent: null for top level).",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
workspace: shared_js_1.workspaceArg,
|
|
147
|
+
collection: (0, shared_js_1.refArg)("Collection"),
|
|
148
|
+
folder: (0, shared_js_1.refArg)("Folder", "(or path)"),
|
|
149
|
+
name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME).optional(),
|
|
150
|
+
parent: zod_1.z.string().max(300).nullable().optional().describe("New parent folder (id, name or path); null = top level"),
|
|
151
|
+
},
|
|
152
|
+
annotations: shared_js_1.IDEMPOTENT_WRITE,
|
|
153
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, name, parent }) => {
|
|
154
|
+
if (name === undefined && parent === undefined)
|
|
155
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or parent.");
|
|
156
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
157
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
158
|
+
const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
|
|
159
|
+
if (!target)
|
|
160
|
+
throw new errors_js_1.ToolInputError("REF_REQUIRED", "folder is required.");
|
|
161
|
+
const parentFolder = parent ? await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, parent) : null;
|
|
162
|
+
if (parentFolder && parentFolder.item.id === target.item.id)
|
|
163
|
+
throw new errors_js_1.ToolInputError("FOLDER_CYCLE", "A folder cannot be its own parent.");
|
|
164
|
+
const updated = await ctx.api.updateFolder(ws.id, target.item.id, {
|
|
165
|
+
...(name !== undefined ? { name: name.trim() } : {}),
|
|
166
|
+
...(parent !== undefined ? { parentId: parentFolder?.item.id ?? null } : {}),
|
|
167
|
+
});
|
|
168
|
+
ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
|
|
169
|
+
return (0, shared_js_1.ok)({ folder: { id: updated.id, name: updated.name, parentId: updated.parentId }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), (0, resolve_js_1.resolvedSummary)(folder, target)] });
|
|
170
|
+
}));
|
|
171
|
+
server.registerTool("delete_collection", {
|
|
172
|
+
title: "Delete a collection",
|
|
173
|
+
description: "Delete a collection with all its folders and requests. Permanent — confirm with the user first.",
|
|
174
|
+
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), confirm: shared_js_1.confirmArg },
|
|
175
|
+
annotations: shared_js_1.DESTRUCTIVE,
|
|
176
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, confirm }) => {
|
|
177
|
+
(0, shared_js_1.requireConfirm)(confirm, "a collection");
|
|
178
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
179
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
180
|
+
await ctx.api.deleteCollection(ws.id, resolved.item.id);
|
|
181
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
182
|
+
ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
|
|
183
|
+
return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)] });
|
|
184
|
+
}));
|
|
185
|
+
server.registerTool("delete_folder", {
|
|
186
|
+
title: "Delete a folder",
|
|
187
|
+
description: "Delete a folder and everything inside it (subfolders and requests). Permanent — confirm with the user first.",
|
|
188
|
+
inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), folder: (0, shared_js_1.refArg)("Folder", "(or path)"), confirm: shared_js_1.confirmArg },
|
|
189
|
+
annotations: shared_js_1.DESTRUCTIVE,
|
|
190
|
+
}, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, confirm }) => {
|
|
191
|
+
(0, shared_js_1.requireConfirm)(confirm, "a folder");
|
|
192
|
+
const ws = await ctx.resolveWorkspace(workspace);
|
|
193
|
+
const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
|
|
194
|
+
const target = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
|
|
195
|
+
if (!target)
|
|
196
|
+
throw new errors_js_1.ToolInputError("REF_REQUIRED", "folder is required.");
|
|
197
|
+
await ctx.api.deleteFolder(ws.id, target.item.id);
|
|
198
|
+
ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
|
|
199
|
+
ctx.invalidate(`collections:${ws.id}`);
|
|
200
|
+
return (0, shared_js_1.ok)({ deleted: { id: target.item.id, name: target.item.name, path: target.item.path }, resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), (0, resolve_js_1.resolvedSummary)(folder, target)] });
|
|
201
|
+
}));
|
|
202
|
+
};
|
|
203
|
+
exports.registerCollectionTools = registerCollectionTools;
|
|
204
|
+
const compactSettings = (settings) => Object.fromEntries(Object.entries(settings).filter(([, value]) => typeof value === "boolean"));
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerDiagramTools = void 0;
|
|
4
|
+
const callman_core_1 = require("callman-core");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const errors_js_1 = require("../client/errors.js");
|
|
7
|
+
const build_js_1 = require("../domain/diagram/build.js");
|
|
8
|
+
const layout_js_1 = require("../domain/diagram/layout.js");
|
|
9
|
+
const patch_js_1 = require("../domain/diagram/patch.js");
|
|
10
|
+
const summarize_js_1 = require("../domain/diagram/summarize.js");
|
|
11
|
+
const resolve_js_1 = require("../domain/resolve.js");
|
|
12
|
+
const ids_js_1 = require("../lib/ids.js");
|
|
13
|
+
const pagination_js_1 = require("../lib/pagination.js");
|
|
14
|
+
const lookups_js_1 = require("./lookups.js");
|
|
15
|
+
const shared_js_1 = require("./shared.js");
|
|
16
|
+
const directionArg = zod_1.z.enum(["DOWN", "RIGHT", "UP", "LEFT"]).optional().describe("Layout flow direction (default DOWN)");
|
|
17
|
+
const loadDocument = (diagram) => {
|
|
18
|
+
const parsed = (0, callman_core_1.safeParseDiagramDocument)(diagram.document);
|
|
19
|
+
if (!parsed.ok) {
|
|
20
|
+
throw new errors_js_1.ToolInputError("DIAGRAM_DOCUMENT_INVALID", `The stored diagram "${diagram.name}" is not a valid document: ${parsed.errors.join("; ")}`, { errors: parsed.errors });
|
|
21
|
+
}
|
|
22
|
+
return parsed.document;
|
|
23
|
+
};
|
|
24
|
+
const registerDiagramTools = (server, ctx) => {
|
|
25
|
+
server.registerTool("list_diagrams", {
|
|
26
|
+
title: "List diagrams",
|
|
27
|
+
description: "List your system-design diagrams and the ones shared with you (owner, node/edge counts, whether you can edit). Diagrams belong to users and are shared into workspaces — not workspace-scoped.",
|
|
28
|
+
inputSchema: { query: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
|
|
29
|
+
annotations: shared_js_1.READ,
|
|
30
|
+
}, (0, shared_js_1.guarded)(async ({ query, cursor, limit }) => {
|
|
31
|
+
const all = await (0, lookups_js_1.loadDiagrams)(ctx);
|
|
32
|
+
const needle = query?.trim().toLowerCase();
|
|
33
|
+
const filtered = needle ? all.filter((diagram) => diagram.name.toLowerCase().includes(needle)) : all;
|
|
34
|
+
const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit);
|
|
35
|
+
return (0, shared_js_1.ok)({ diagrams: page.items.map(summarize_js_1.diagramListView), total: page.total, nextCursor: page.nextCursor });
|
|
36
|
+
}));
|
|
37
|
+
server.registerTool("get_diagram", {
|
|
38
|
+
title: "Get a diagram",
|
|
39
|
+
description: "Read a diagram: `graph` (nodes with type/label/parent and edges with their meaning — enough to describe or edit it) or `full` (the complete DiagramDocument, up to 512 KB).",
|
|
40
|
+
inputSchema: { diagram: (0, shared_js_1.refArg)("Diagram"), view: zod_1.z.enum(["summary", "graph", "full"]).default("graph") },
|
|
41
|
+
annotations: shared_js_1.READ,
|
|
42
|
+
}, (0, shared_js_1.guarded)(async ({ diagram, view }) => {
|
|
43
|
+
const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
|
|
44
|
+
const doc = loadDocument(resolved.item);
|
|
45
|
+
const summary = (0, summarize_js_1.diagramListView)(resolved.item);
|
|
46
|
+
const body = view === "summary" ? summary : view === "graph" ? { ...summary, ...(0, summarize_js_1.diagramGraphView)(doc) } : { ...summary, ...(0, summarize_js_1.diagramFullView)(doc) };
|
|
47
|
+
return (0, shared_js_1.ok)({ diagram: body, ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] } : {}) });
|
|
48
|
+
}));
|
|
49
|
+
server.registerTool("create_diagram", {
|
|
50
|
+
title: "Create a system-design diagram",
|
|
51
|
+
description: "Create a diagram from typed nodes and edges. Node `type` must come from callman://diagram/components (e.g. net.service, net.gateway, db.postgres, mq.kafka, aws.lambda, flow.process); unknown types are rejected with suggestions unless strictTypes: false. Group nodes with a container (`parent`). Edge `semantic`: sync (default), async, fallback, bus. Layout is automatic when nodes have no positions. Keep it readable: 8–25 nodes, short labels.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
name: zod_1.z.string().min(1).max(200).describe("Diagram name (library entry)"),
|
|
54
|
+
...build_js_1.diagramInputSchema.shape,
|
|
55
|
+
autoLayout: zod_1.z.boolean().optional().describe("Force layout even when positions were given"),
|
|
56
|
+
direction: directionArg,
|
|
57
|
+
strictTypes: zod_1.z.boolean().optional(),
|
|
58
|
+
dryRun: shared_js_1.dryRunArg,
|
|
59
|
+
},
|
|
60
|
+
annotations: shared_js_1.WRITE,
|
|
61
|
+
}, (0, shared_js_1.guarded)(async ({ name, autoLayout, direction, strictTypes, dryRun, ...input }) => {
|
|
62
|
+
const built = (0, build_js_1.buildDiagramDocument)(input, { strictTypes });
|
|
63
|
+
let document = built.document;
|
|
64
|
+
let layoutNote;
|
|
65
|
+
if (autoLayout || built.unpositionedNodeIds.length > 0) {
|
|
66
|
+
const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
|
|
67
|
+
document = laid.document;
|
|
68
|
+
layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
|
|
69
|
+
}
|
|
70
|
+
if (dryRun) {
|
|
71
|
+
return (0, shared_js_1.ok)({ dryRun: true, diagram: { name, ...(0, summarize_js_1.diagramGraphView)(document) }, warnings: built.warnings, ...(layoutNote ? { layout: layoutNote } : {}) });
|
|
72
|
+
}
|
|
73
|
+
const created = await ctx.api.createDiagram({ name: name.trim(), document });
|
|
74
|
+
ctx.invalidate("diagrams");
|
|
75
|
+
return (0, shared_js_1.ok)({
|
|
76
|
+
diagram: { ...(0, summarize_js_1.diagramListView)(created), ...(0, summarize_js_1.diagramGraphView)(document) },
|
|
77
|
+
warnings: built.warnings,
|
|
78
|
+
...(layoutNote ? { layout: layoutNote } : {}),
|
|
79
|
+
note: "Saved. Open it in the desktop app (Diagrams → Refresh). Edit with patch_diagram; share with share_diagram.",
|
|
80
|
+
});
|
|
81
|
+
}));
|
|
82
|
+
server.registerTool("update_diagram", {
|
|
83
|
+
title: "Replace a diagram's content",
|
|
84
|
+
description: "Replace a diagram's whole document (nodes + edges) and/or rename it, keeping its identity and sharing. For small edits prefer patch_diagram.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
diagram: (0, shared_js_1.refArg)("Diagram"),
|
|
87
|
+
name: zod_1.z.string().min(1).max(200).optional(),
|
|
88
|
+
title: zod_1.z.string().max(200).optional(),
|
|
89
|
+
nodes: build_js_1.diagramInputSchema.shape.nodes.optional(),
|
|
90
|
+
edges: build_js_1.diagramInputSchema.shape.edges.optional(),
|
|
91
|
+
canvas: build_js_1.diagramInputSchema.shape.canvas,
|
|
92
|
+
autoLayout: zod_1.z.boolean().optional(),
|
|
93
|
+
direction: directionArg,
|
|
94
|
+
strictTypes: zod_1.z.boolean().optional(),
|
|
95
|
+
},
|
|
96
|
+
annotations: shared_js_1.IDEMPOTENT_WRITE,
|
|
97
|
+
}, (0, shared_js_1.guarded)(async ({ diagram, name, title, nodes, edges, canvas, autoLayout, direction, strictTypes }) => {
|
|
98
|
+
if (name === undefined && nodes === undefined)
|
|
99
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or nodes (+ edges).");
|
|
100
|
+
const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
|
|
101
|
+
if (!resolved.item.canEdit)
|
|
102
|
+
throw new errors_js_1.ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
|
|
103
|
+
const existing = loadDocument(resolved.item);
|
|
104
|
+
let document;
|
|
105
|
+
let layoutNote;
|
|
106
|
+
let warnings = [];
|
|
107
|
+
if (nodes) {
|
|
108
|
+
const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? existing.meta.title, nodes, edges: edges ?? [], canvas: canvas ?? existing.canvas }, { strictTypes, existingId: existing.id, createdAt: existing.meta.createdAt });
|
|
109
|
+
document = built.document;
|
|
110
|
+
warnings = built.warnings;
|
|
111
|
+
if (autoLayout || built.unpositionedNodeIds.length > 0) {
|
|
112
|
+
const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
|
|
113
|
+
document = laid.document;
|
|
114
|
+
layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (title !== undefined) {
|
|
118
|
+
document = { ...existing, meta: { ...existing.meta, title, updatedAt: new Date().toISOString() } };
|
|
119
|
+
}
|
|
120
|
+
const updated = await ctx.api.updateDiagram(resolved.item.id, { ...(name !== undefined ? { name: name.trim() } : {}), ...(document ? { document } : {}) });
|
|
121
|
+
ctx.invalidate("diagrams");
|
|
122
|
+
return (0, shared_js_1.ok)({ diagram: { ...(0, summarize_js_1.diagramListView)(updated), ...(0, summarize_js_1.diagramGraphView)(document ?? existing) }, warnings, ...(layoutNote ? { layout: layoutNote } : {}), resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] });
|
|
123
|
+
}));
|
|
124
|
+
server.registerTool("patch_diagram", {
|
|
125
|
+
title: "Edit a diagram incrementally",
|
|
126
|
+
description: "Apply operations to a diagram without resending it: addNode, updateNode, removeNode (drops its edges), addEdge, updateEdge, removeEdge, setTitle. `relayout`: none | new (place only new nodes, default) | all (re-run automatic layout).",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
diagram: (0, shared_js_1.refArg)("Diagram"),
|
|
129
|
+
ops: zod_1.z.array(patch_js_1.diagramPatchOpSchema).min(1).max(200),
|
|
130
|
+
relayout: zod_1.z.enum(["none", "new", "all"]).default("new"),
|
|
131
|
+
direction: directionArg,
|
|
132
|
+
strictTypes: zod_1.z.boolean().optional(),
|
|
133
|
+
},
|
|
134
|
+
annotations: shared_js_1.IDEMPOTENT_WRITE,
|
|
135
|
+
}, (0, shared_js_1.guarded)(async ({ diagram, ops, relayout, direction, strictTypes }) => {
|
|
136
|
+
const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
|
|
137
|
+
if (!resolved.item.canEdit)
|
|
138
|
+
throw new errors_js_1.ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
|
|
139
|
+
const existing = loadDocument(resolved.item);
|
|
140
|
+
const patched = (0, patch_js_1.applyDiagramPatch)(existing, ops, { strictTypes });
|
|
141
|
+
let document = patched.document;
|
|
142
|
+
let layoutNote;
|
|
143
|
+
if (relayout === "all") {
|
|
144
|
+
const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
|
|
145
|
+
document = laid.document;
|
|
146
|
+
layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
|
|
147
|
+
}
|
|
148
|
+
else if (relayout === "new" && patched.addedNodeIds.length > 0) {
|
|
149
|
+
document = (0, layout_js_1.placeNewNodes)(document, patched.addedNodeIds);
|
|
150
|
+
layoutNote = `Placed ${patched.addedNodeIds.length} new node(s) below the existing ones.`;
|
|
151
|
+
}
|
|
152
|
+
const updated = await ctx.api.updateDiagram(resolved.item.id, { document });
|
|
153
|
+
ctx.invalidate("diagrams");
|
|
154
|
+
return (0, shared_js_1.ok)({
|
|
155
|
+
diagram: { ...(0, summarize_js_1.diagramListView)(updated), ...(0, summarize_js_1.diagramGraphView)(document) },
|
|
156
|
+
applied: ops.length,
|
|
157
|
+
added: patched.addedNodeIds,
|
|
158
|
+
removedNodes: patched.removedNodeIds,
|
|
159
|
+
removedEdges: patched.removedEdgeIds,
|
|
160
|
+
warnings: patched.warnings,
|
|
161
|
+
...(layoutNote ? { layout: layoutNote } : {}),
|
|
162
|
+
resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)],
|
|
163
|
+
});
|
|
164
|
+
}));
|
|
165
|
+
server.registerTool("delete_diagram", {
|
|
166
|
+
title: "Delete a diagram",
|
|
167
|
+
description: "Delete a diagram you own. Permanent — confirm with the user first.",
|
|
168
|
+
inputSchema: { diagram: (0, shared_js_1.refArg)("Diagram"), confirm: shared_js_1.confirmArg },
|
|
169
|
+
annotations: shared_js_1.DESTRUCTIVE,
|
|
170
|
+
}, (0, shared_js_1.guarded)(async ({ diagram, confirm }) => {
|
|
171
|
+
(0, shared_js_1.requireConfirm)(confirm, "a diagram");
|
|
172
|
+
const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
|
|
173
|
+
await ctx.api.deleteDiagram(resolved.item.id);
|
|
174
|
+
ctx.invalidate("diagrams");
|
|
175
|
+
return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] });
|
|
176
|
+
}));
|
|
177
|
+
server.registerTool("share_diagram", {
|
|
178
|
+
title: "Share a diagram",
|
|
179
|
+
description: "Share a diagram into workspaces (every member can view it) and grant edit access to specific people (by email — they must be members of one of your workspaces). Additive by default; replace: true sets exactly the given lists (an empty list unshares).",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
diagram: (0, shared_js_1.refArg)("Diagram"),
|
|
182
|
+
workspaces: zod_1.z.array(zod_1.z.string().min(1).max(200)).max(200).optional().describe("Workspace names or ids to share into"),
|
|
183
|
+
editors: zod_1.z.array(zod_1.z.string().min(1).max(255)).max(500).optional().describe("Emails (or user ids) who may edit"),
|
|
184
|
+
replace: zod_1.z.boolean().optional(),
|
|
185
|
+
},
|
|
186
|
+
annotations: shared_js_1.IDEMPOTENT_WRITE,
|
|
187
|
+
}, (0, shared_js_1.guarded)(async ({ diagram, workspaces, editors, replace }) => {
|
|
188
|
+
if (!workspaces && !editors)
|
|
189
|
+
throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass workspaces and/or editors.");
|
|
190
|
+
const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
|
|
191
|
+
const workspaceIds = new Set(replace ? [] : resolved.item.sharedWorkspaceIds ?? []);
|
|
192
|
+
const editorIds = new Set(replace ? [] : resolved.item.editorIds ?? []);
|
|
193
|
+
const resolvedNotes = [];
|
|
194
|
+
for (const ref of workspaces ?? []) {
|
|
195
|
+
const ws = await ctx.resolveWorkspace(ref);
|
|
196
|
+
workspaceIds.add(ws.id);
|
|
197
|
+
resolvedNotes.push({ input: ref, matched: ws.name, id: ws.id });
|
|
198
|
+
}
|
|
199
|
+
if (editors && editors.length > 0) {
|
|
200
|
+
// Look up emails across the user's workspaces (members are the only
|
|
201
|
+
// people a diagram can be shared with anyway).
|
|
202
|
+
const memberByEmail = new Map();
|
|
203
|
+
const memberIds = new Set();
|
|
204
|
+
for (const ws of await ctx.listWorkspaces()) {
|
|
205
|
+
try {
|
|
206
|
+
const { items } = await ctx.api.listMembers(ws.id);
|
|
207
|
+
for (const member of items) {
|
|
208
|
+
memberByEmail.set((0, ids_js_1.normalizeName)(member.email), member.userId);
|
|
209
|
+
memberIds.add(member.userId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// no access to the member list of that workspace — skip it
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const ref of editors) {
|
|
217
|
+
const userId = memberByEmail.get((0, ids_js_1.normalizeName)(ref)) ?? (memberIds.has(ref) ? ref : null);
|
|
218
|
+
if (!userId)
|
|
219
|
+
throw new errors_js_1.ToolInputError("EDITOR_NOT_FOUND", `No member with email or id "${ref}" in your workspaces.`);
|
|
220
|
+
editorIds.add(userId);
|
|
221
|
+
resolvedNotes.push({ input: ref, matched: ref, id: userId });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const updated = await ctx.api.shareDiagram(resolved.item.id, { workspaceIds: [...workspaceIds], editorIds: [...editorIds] });
|
|
225
|
+
ctx.invalidate("diagrams");
|
|
226
|
+
return (0, shared_js_1.ok)({ diagram: (0, summarize_js_1.diagramListView)(updated), sharedWorkspaceIds: updated.sharedWorkspaceIds, editorIds: updated.editorIds, resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved), ...resolvedNotes] });
|
|
227
|
+
}));
|
|
228
|
+
};
|
|
229
|
+
exports.registerDiagramTools = registerDiagramTools;
|