callman-mcp 0.1.1 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +18 -4
  3. package/dist/context.d.ts +2 -0
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js +7 -2
  6. package/dist/context.js.map +1 -1
  7. package/dist/resources/docs/overview.d.ts +1 -1
  8. package/dist/resources/docs/overview.d.ts.map +1 -1
  9. package/dist/resources/docs/overview.js +15 -2
  10. package/dist/resources/docs/overview.js.map +1 -1
  11. package/dist/resources/index.d.ts.map +1 -1
  12. package/dist/resources/index.js +10 -13
  13. package/dist/resources/index.js.map +1 -1
  14. package/dist/server.d.ts +1 -1
  15. package/dist/server.d.ts.map +1 -1
  16. package/dist/server.js +1 -1
  17. package/dist/server.js.map +1 -1
  18. package/dist/tools/catalog.d.ts.map +1 -1
  19. package/dist/tools/catalog.js +1 -0
  20. package/dist/tools/catalog.js.map +1 -1
  21. package/dist/tools/collections.d.ts.map +1 -1
  22. package/dist/tools/collections.js +27 -30
  23. package/dist/tools/collections.js.map +1 -1
  24. package/dist/tools/contracts.d.ts.map +1 -1
  25. package/dist/tools/contracts.js +35 -26
  26. package/dist/tools/contracts.js.map +1 -1
  27. package/dist/tools/environments.d.ts.map +1 -1
  28. package/dist/tools/environments.js +32 -31
  29. package/dist/tools/environments.js.map +1 -1
  30. package/dist/tools/import.d.ts.map +1 -1
  31. package/dist/tools/import.js +9 -5
  32. package/dist/tools/import.js.map +1 -1
  33. package/dist/tools/lookups.d.ts +26 -1
  34. package/dist/tools/lookups.d.ts.map +1 -1
  35. package/dist/tools/lookups.js +95 -2
  36. package/dist/tools/lookups.js.map +1 -1
  37. package/dist/tools/meta.d.ts.map +1 -1
  38. package/dist/tools/meta.js +9 -4
  39. package/dist/tools/meta.js.map +1 -1
  40. package/dist/tools/requests.d.ts.map +1 -1
  41. package/dist/tools/requests.js +49 -29
  42. package/dist/tools/requests.js.map +1 -1
  43. package/dist/tools/run.d.ts.map +1 -1
  44. package/dist/tools/run.js +9 -6
  45. package/dist/tools/run.js.map +1 -1
  46. package/dist/tools/scenarios.d.ts.map +1 -1
  47. package/dist/tools/scenarios.js +8 -3
  48. package/dist/tools/scenarios.js.map +1 -1
  49. package/dist/tools/shared.d.ts.map +1 -1
  50. package/dist/tools/shared.js +1 -1
  51. package/dist/tools/shared.js.map +1 -1
  52. package/dist/tools/workspaces.d.ts.map +1 -1
  53. package/dist/tools/workspaces.js +18 -2
  54. package/dist/tools/workspaces.js.map +1 -1
  55. package/dist-cjs/context.js +7 -2
  56. package/dist-cjs/resources/docs/overview.js +15 -2
  57. package/dist-cjs/resources/index.js +9 -12
  58. package/dist-cjs/server.js +1 -1
  59. package/dist-cjs/tools/catalog.js +1 -0
  60. package/dist-cjs/tools/collections.js +26 -29
  61. package/dist-cjs/tools/contracts.js +34 -25
  62. package/dist-cjs/tools/environments.js +31 -30
  63. package/dist-cjs/tools/import.js +8 -4
  64. package/dist-cjs/tools/lookups.js +103 -3
  65. package/dist-cjs/tools/meta.js +9 -4
  66. package/dist-cjs/tools/requests.js +48 -28
  67. package/dist-cjs/tools/run.js +8 -5
  68. package/dist-cjs/tools/scenarios.js +8 -3
  69. package/dist-cjs/tools/shared.js +1 -1
  70. package/dist-cjs/tools/workspaces.js +18 -2
  71. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sameName = exports.connectionResolverFor = exports.loadConnections = exports.resolveDiagram = exports.loadDiagrams = exports.resolveScenarioFolder = exports.resolveScenario = exports.loadScenarioCatalog = exports.resolveEnvironment = exports.loadEnvironments = exports.resolveRequest = exports.searchRequests = exports.requestIdsInFolder = exports.resolveFolder = exports.loadTree = exports.resolveCollection = exports.loadCollections = void 0;
3
+ exports.sameName = exports.connectionResolverFor = exports.loadConnections = exports.resolveDiagram = exports.loadDiagrams = exports.resolveScenarioFolder = exports.resolveScenario = exports.loadScenarioCatalog = exports.resolveEnvironment = exports.loadEnvironments = exports.resolveRequest = exports.searchRequests = exports.requestIdsInFolder = exports.resolveFolder = exports.loadTree = exports.resolveCollection = exports.findEnvironment = exports.findRequest = exports.findCollection = exports.findInWorkspaces = exports.workspaceTag = exports.workspacesForListing = exports.isAllWorkspaces = exports.loadCollections = void 0;
4
+ const errors_js_1 = require("../client/errors.js");
4
5
  const requestIntegrations_js_1 = require("../domain/requestIntegrations.js");
5
6
  const resolve_js_1 = require("../domain/resolve.js");
6
7
  const ids_js_1 = require("../lib/ids.js");
@@ -10,6 +11,105 @@ const truncate_js_1 = require("../lib/truncate.js");
10
11
  // prefixes it touches so the next read is fresh.
11
12
  const loadCollections = (ctx, ws) => ctx.cache.getOrSet(`collections:${ws}`, () => ctx.api.listAllCollections(ws));
12
13
  exports.loadCollections = loadCollections;
14
+ // ── across workspaces ──────────────────────────────────────────────────
15
+ // A personal token acts in EVERY workspace the user belongs to. Reads span
16
+ // all of them; the session default only orders them and wins ties.
17
+ // `workspace: "all"` (or "*") asks a listing for every workspace explicitly.
18
+ const isAllWorkspaces = (ref) => Boolean(ref && ["all", "*", "any"].includes(ref.trim().toLowerCase()));
19
+ exports.isAllWorkspaces = isAllWorkspaces;
20
+ const workspacesForListing = async (ctx, explicit) => {
21
+ if (explicit && explicit.trim() && !(0, exports.isAllWorkspaces)(explicit))
22
+ return [await ctx.resolveWorkspace(explicit)];
23
+ const workspaces = await ctx.listWorkspaces();
24
+ const preferred = ctx.getDefaultWorkspaceId();
25
+ return [...workspaces].sort((a, b) => Number(b.id === preferred) - Number(a.id === preferred));
26
+ };
27
+ exports.workspacesForListing = workspacesForListing;
28
+ const workspaceTag = (ws) => ({ id: ws.id, name: ws.name });
29
+ exports.workspaceTag = workspaceTag;
30
+ // Find something the model named (a collection, request, environment…).
31
+ // `workspace` given → only there. Otherwise the default workspace first — an
32
+ // exact or id match there wins at once; then every other workspace: a unique
33
+ // match anywhere is used, the same name in several workspaces comes back as
34
+ // candidates tagged with their workspace. A workspace where the thing is
35
+ // missing (404 / REF_NOT_FOUND) is simply skipped.
36
+ const findInWorkspaces = async (ctx, explicit, kind, ref, find, options = {}) => {
37
+ if (explicit && explicit.trim() && !(0, exports.isAllWorkspaces)(explicit)) {
38
+ const ws = await ctx.resolveWorkspace(explicit);
39
+ return { ws, value: await find(ws) };
40
+ }
41
+ const order = await (0, exports.workspacesForListing)(ctx, null);
42
+ if (order.length === 0)
43
+ throw new errors_js_1.ToolInputError("WORKSPACE_NONE", "You are not a member of any workspace.");
44
+ const preferred = ctx.getDefaultWorkspaceId();
45
+ const hits = [];
46
+ const ambiguous = [];
47
+ for (const ws of order) {
48
+ let value;
49
+ try {
50
+ value = await find(ws);
51
+ }
52
+ catch (error) {
53
+ if (error instanceof errors_js_1.ToolInputError && error.code === "REF_NOT_FOUND")
54
+ continue;
55
+ if (error instanceof errors_js_1.ToolInputError && error.code === "REF_AMBIGUOUS") {
56
+ ambiguous.push({ ws, error });
57
+ continue;
58
+ }
59
+ if (error instanceof errors_js_1.CallmanApiError && (error.status === 404 || error.status === 403))
60
+ continue;
61
+ throw error;
62
+ }
63
+ const matched = options.matchOf?.(value);
64
+ if (matched === "id" || (ws.id === preferred && matched !== "partial")) {
65
+ return { ws, value, ...(ws.id !== preferred && order.length > 1 ? { note: `found in workspace "${ws.name}"` } : {}) };
66
+ }
67
+ hits.push({ ws, value, matched });
68
+ }
69
+ const exact = hits.filter((hit) => hit.matched !== "partial");
70
+ const pool = exact.length > 0 ? exact : hits;
71
+ // Ambiguous inside ONE workspace and nothing elsewhere: that workspace's
72
+ // own candidate list, tagged with where it is.
73
+ if (pool.length === 0 && ambiguous.length === 1) {
74
+ const [{ ws, error }] = ambiguous;
75
+ throw new errors_js_1.ToolInputError(error.code, order.length > 1 ? `${error.message} (in workspace "${ws.name}")` : error.message, { ...(error.data ?? {}), workspace: (0, exports.workspaceTag)(ws) });
76
+ }
77
+ if (pool.length === 1 && ambiguous.length === 0) {
78
+ const [hit] = pool;
79
+ return { ws: hit.ws, value: hit.value, ...(hit.ws.id !== preferred && order.length > 1 ? { note: `found in workspace "${hit.ws.name}"` } : {}) };
80
+ }
81
+ if (pool.length > 1 || ambiguous.length > 0) {
82
+ throw new errors_js_1.ToolInputError("REF_AMBIGUOUS", `"${ref}" matches ${kind}s in several places — pass \`workspace\` (name or id), or use the id.`, {
83
+ kind,
84
+ ref,
85
+ candidates: [
86
+ ...pool.map((hit) => ({ workspace: (0, exports.workspaceTag)(hit.ws), ...(options.describe?.(hit.value) ?? {}) })),
87
+ ...ambiguous.map((entry) => ({ workspace: (0, exports.workspaceTag)(entry.ws), candidates: entry.error.data?.candidates })),
88
+ ],
89
+ });
90
+ }
91
+ throw new errors_js_1.ToolInputError("REF_NOT_FOUND", `No ${kind} matches "${ref}" in ${order.length === 1 ? "your workspace" : `any of your ${order.length} workspaces`}.`, {
92
+ kind,
93
+ ref,
94
+ searchedWorkspaces: order.map(exports.workspaceTag),
95
+ });
96
+ };
97
+ exports.findInWorkspaces = findInWorkspaces;
98
+ const findCollection = (ctx, workspace, ref, options = {}) => (0, exports.findInWorkspaces)(ctx, workspace, "collection", ref, (ws) => (0, exports.resolveCollection)(ctx, ws.id, ref, options), {
99
+ matchOf: (resolved) => resolved.matched,
100
+ describe: (resolved) => ({ id: resolved.item.id, name: resolved.item.name }),
101
+ });
102
+ exports.findCollection = findCollection;
103
+ const findRequest = (ctx, workspace, ref, collectionRef, options = {}) => (0, exports.findInWorkspaces)(ctx, workspace, "request", ref, (ws) => (0, exports.resolveRequest)(ctx, ws.id, ref, collectionRef, options), {
104
+ matchOf: (resolved) => resolved.matched,
105
+ describe: (resolved) => ({ id: resolved.request.id, name: resolved.request.name, method: resolved.request.method, url: resolved.request.url }),
106
+ });
107
+ exports.findRequest = findRequest;
108
+ const findEnvironment = (ctx, workspace, ref, options = {}) => (0, exports.findInWorkspaces)(ctx, workspace, "environment", ref, (ws) => (0, exports.resolveEnvironment)(ctx, ws.id, ref, options), {
109
+ matchOf: (resolved) => resolved.matched,
110
+ describe: (resolved) => ({ id: resolved.item.id, name: resolved.item.name }),
111
+ });
112
+ exports.findEnvironment = findEnvironment;
13
113
  const resolveCollection = async (ctx, ws, ref, options = {}) => {
14
114
  const collections = await (0, exports.loadCollections)(ctx, ws);
15
115
  if ((0, ids_js_1.isObjectId)(ref) && !collections.some((collection) => collection.id === ref)) {
@@ -89,7 +189,7 @@ exports.searchRequests = searchRequests;
89
189
  // given collection, or across the workspace when no collection is given.
90
190
  const resolveRequest = async (ctx, ws, ref, collectionRef, options = {}) => {
91
191
  if ((0, ids_js_1.isObjectId)(ref)) {
92
- return { request: await ctx.api.getRequest(ws, ref) };
192
+ return { request: await ctx.api.getRequest(ws, ref), matched: "id" };
93
193
  }
94
194
  const collectionId = collectionRef ? (await (0, exports.resolveCollection)(ctx, ws, collectionRef)).item.id : null;
95
195
  const collections = await (0, exports.loadCollections)(ctx, ws);
@@ -105,7 +205,7 @@ const resolveRequest = async (ctx, ws, ref, collectionRef, options = {}) => {
105
205
  aliases: (request) => [`${request.method} ${request.url}`, request.url],
106
206
  exact: options.exact,
107
207
  });
108
- return { request: await ctx.api.getRequest(ws, resolved.item.id), annotation: resolved.annotation };
208
+ return { request: await ctx.api.getRequest(ws, resolved.item.id), annotation: resolved.annotation, matched: resolved.matched };
109
209
  };
110
210
  exports.resolveRequest = resolveRequest;
111
211
  const loadEnvironments = (ctx, ws) => ctx.cache.getOrSet(`environments:${ws}`, () => ctx.api.listAllEnvironments(ws));
@@ -46,8 +46,9 @@ const registerMetaTools = (server, ctx) => {
46
46
  const me = meResult.value;
47
47
  const workspaces = workspacesResult.status === "fulfilled" ? workspacesResult.value : [];
48
48
  const features = featuresResult.status === "fulfilled" ? featuresResult.value.features : {};
49
- const defaultWorkspace = ctx.config.defaultWorkspaceId
50
- ? (workspaces.find((workspace) => workspace.id === ctx.config.defaultWorkspaceId) ?? null)
49
+ const preferred = ctx.getDefaultWorkspaceId();
50
+ const defaultWorkspace = preferred
51
+ ? (workspaces.find((workspace) => workspace.id === preferred) ?? null)
51
52
  : workspaces.length === 1
52
53
  ? workspaces[0]
53
54
  : null;
@@ -69,8 +70,12 @@ const registerMetaTools = (server, ctx) => {
69
70
  isDefault: workspace.id === defaultWorkspace?.id,
70
71
  })),
71
72
  defaultWorkspaceId: defaultWorkspace?.id ?? null,
72
- ...(workspaces.length > 1 && !defaultWorkspace
73
- ? { note: "Several workspaces and no default: pass `workspace` to tools or set CALLMAN_WORKSPACE_ID." }
73
+ ...(workspaces.length > 1
74
+ ? {
75
+ workspaceScope: `This token works in all ${workspaces.length} of your workspaces. Lists and searches cover every workspace unless you pass \`workspace\`; a name is looked up in the default workspace first, then in all others. ${defaultWorkspace
76
+ ? `The default ("${defaultWorkspace.name}") only decides where NEW things go and wins a name that exists in several workspaces`
77
+ : "There is no default, so creating something needs `workspace`"} — change it for this session with use_workspace.`,
78
+ }
74
79
  : {}),
75
80
  features,
76
81
  localRun: ctx.config.allowLocalRun,
@@ -15,7 +15,7 @@ const MOVE_MAX = 200;
15
15
  const registerRequestTools = (server, ctx) => {
16
16
  server.registerTool("search_requests", {
17
17
  title: "Search requests",
18
- description: "Find saved requests by words in their name, method or URL (all words must match), across the workspace or inside one collection. Returns up to 50 matches with ids you can pass to get_request / run_request.",
18
+ description: "Find saved requests by words in their name, method or URL (all words must match) — across ALL your workspaces, one `workspace`, or inside one collection. Returns up to 50 matches (each with its collection and workspace) with ids you can pass to get_request / run_request.",
19
19
  inputSchema: {
20
20
  workspace: shared_js_1.workspaceArg,
21
21
  query: zod_1.z.string().min(1).max(200).describe("e.g. \"POST login\", \"users\", \"GET /orders\""),
@@ -24,14 +24,23 @@ const registerRequestTools = (server, ctx) => {
24
24
  },
25
25
  annotations: shared_js_1.READ,
26
26
  }, (0, shared_js_1.guarded)(async ({ workspace, query, collection, limit }) => {
27
- const ws = await ctx.resolveWorkspace(workspace);
28
- const scope = collection ? await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection) : null;
29
- const matches = await (0, lookups_js_1.searchRequests)(ctx, ws.id, query, scope?.item.id ?? null, limit ?? truncate_js_1.LIMITS.SEARCH_RESULTS);
27
+ const max = limit ?? truncate_js_1.LIMITS.SEARCH_RESULTS;
28
+ const scoped = collection ? await (0, lookups_js_1.findCollection)(ctx, workspace, collection) : null;
29
+ const workspaces = scoped ? [scoped.ws] : await (0, lookups_js_1.workspacesForListing)(ctx, workspace);
30
+ const matches = [];
31
+ for (const ws of workspaces) {
32
+ if (matches.length >= max)
33
+ break;
34
+ for (const match of await (0, lookups_js_1.searchRequests)(ctx, ws.id, query, scoped?.value.item.id ?? null, max - matches.length))
35
+ matches.push({ ...match, ws });
36
+ }
37
+ const scope = scoped?.value ?? null;
30
38
  return (0, shared_js_1.ok)({
31
39
  query,
32
40
  ...(scope ? { collection: { id: scope.item.id, name: scope.item.name } } : {}),
33
- matches: matches.map((request) => ({ ...(0, requestShape_js_1.requestSummaryView)(request), collection: request.collectionName })),
34
- ...(matches.length >= (limit ?? truncate_js_1.LIMITS.SEARCH_RESULTS) ? { truncated: true } : {}),
41
+ ...(workspaces.length > 1 ? { searchedWorkspaces: workspaces.length } : { workspace: (0, lookups_js_1.workspaceTag)(workspaces[0]) }),
42
+ matches: matches.map((request) => ({ ...(0, requestShape_js_1.requestSummaryView)(request), collection: request.collectionName, ...(workspaces.length > 1 ? { workspace: (0, lookups_js_1.workspaceTag)(request.ws) } : {}) })),
43
+ ...(matches.length >= max ? { truncated: true } : {}),
35
44
  });
36
45
  }));
37
46
  server.registerTool("get_request", {
@@ -46,9 +55,13 @@ const registerRequestTools = (server, ctx) => {
46
55
  },
47
56
  annotations: shared_js_1.READ,
48
57
  }, (0, shared_js_1.guarded)(async ({ workspace, request, collection, full, reveal }) => {
49
- const ws = await ctx.resolveWorkspace(workspace);
50
- const { request: found, annotation } = await (0, lookups_js_1.resolveRequest)(ctx, ws.id, request, collection);
51
- return (0, shared_js_1.ok)({ request: (0, requestShape_js_1.compactRequestView)(found, { full, reveal }), ...(annotation ? { resolved: [{ input: request, matched: found.name, id: found.id, note: annotation }] } : {}) });
58
+ const { ws, value, note } = await (0, lookups_js_1.findRequest)(ctx, workspace, request, collection);
59
+ const { request: found, annotation } = value;
60
+ return (0, shared_js_1.ok)({
61
+ request: (0, requestShape_js_1.compactRequestView)(found, { full, reveal }),
62
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
63
+ ...(annotation || note ? { resolved: [{ input: request, matched: found.name, id: found.id, note: [annotation, note].filter(Boolean).join("; ") }] } : {}),
64
+ });
52
65
  }));
53
66
  server.registerTool("create_request", {
54
67
  title: "Create a request",
@@ -56,8 +69,7 @@ const registerRequestTools = (server, ctx) => {
56
69
  inputSchema: { workspace: shared_js_1.workspaceArg, collection: (0, shared_js_1.refArg)("Collection"), ...requestShape_js_1.requestInputSchema.omit({ folderKey: true }).shape },
57
70
  annotations: shared_js_1.WRITE,
58
71
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, ...item }) => {
59
- const ws = await ctx.resolveWorkspace(workspace);
60
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
72
+ const { ws, value: resolved, note } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
61
73
  const folder = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, item.folder);
62
74
  const input = (0, requestShape_js_1.resolveMaskedValues)(item);
63
75
  const resolve = await (0, lookups_js_1.connectionResolverFor)(ctx, ws.id, (0, requestIntegrations_js_1.hasIntegrations)(input));
@@ -70,9 +82,10 @@ const registerRequestTools = (server, ctx) => {
70
82
  return (0, shared_js_1.ok)({
71
83
  request: { ...(0, requestShape_js_1.requestSummaryView)(created), isEnabledInRunner: created.isEnabledInRunner !== false },
72
84
  collection: { id: resolved.item.id, name: resolved.item.name },
85
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
73
86
  ...(folder ? { folder: { id: folder.item.id, path: folder.item.path } } : {}),
74
87
  ...(warnings.length > 0 ? { warnings } : {}),
75
- resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(folder && item.folder ? [(0, resolve_js_1.resolvedSummary)(item.folder, folder)] : [])],
88
+ resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(note ? [{ input: collection, note }] : []), ...(folder && item.folder ? [(0, resolve_js_1.resolvedSummary)(item.folder, folder)] : [])],
76
89
  });
77
90
  }));
78
91
  server.registerTool("create_requests_bulk", {
@@ -99,7 +112,9 @@ const registerRequestTools = (server, ctx) => {
99
112
  if ((collectionName ? 1 : 0) + (collection ? 1 : 0) !== 1) {
100
113
  throw new errors_js_1.ToolInputError("TARGET_REQUIRED", "Pass exactly one of collectionName (new collection) or collection (existing).");
101
114
  }
102
- const ws = await ctx.resolveWorkspace(workspace);
115
+ // An existing collection decides the workspace; a new one needs a target.
116
+ const target = collection ? await (0, lookups_js_1.findCollection)(ctx, workspace, collection) : null;
117
+ const ws = target ? target.ws : await ctx.resolveWorkspace(workspace);
103
118
  const folderKeys = new Set((folders ?? []).map((folder) => folder.key));
104
119
  for (const folder of folders ?? []) {
105
120
  if (folder.parentKey && !folderKeys.has(folder.parentKey)) {
@@ -163,13 +178,14 @@ const registerRequestTools = (server, ctx) => {
163
178
  }
164
179
  return (0, shared_js_1.ok)({
165
180
  collection: { id: result.collection.id, name: result.collection.name },
181
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
166
182
  created: result.importedCounts,
167
183
  ...(runnerOff.length > 0 ? { disabledInRunner: runnerOff } : {}),
168
184
  ...(runnerWarnings.length > 0 ? { warnings: runnerWarnings } : {}),
169
185
  note: "Created atomically. Open the collection in the desktop app (it refreshes when it regains focus).",
170
186
  });
171
187
  }
172
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
188
+ const resolved = target.value;
173
189
  const tree = await (0, lookups_js_1.loadTree)(ctx, ws.id, resolved.item.id);
174
190
  // Resolve every folder reference up front so a typo fails before any write.
175
191
  const folderIdByRef = new Map();
@@ -215,7 +231,8 @@ const registerRequestTools = (server, ctx) => {
215
231
  requests: created,
216
232
  ...(warnings.length > 0 ? { warnings } : {}),
217
233
  ...(duplicates.length > 0 ? { note: `${duplicates.length} request name(s) already existed in the same folder and were created again: ${duplicates.map((request) => request.name).join(", ")}` } : {}),
218
- resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)],
234
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
235
+ resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved), ...(target?.note ? [{ input: collection, note: target.note }] : [])],
219
236
  });
220
237
  }));
221
238
  server.registerTool("update_request", {
@@ -228,8 +245,8 @@ const registerRequestTools = (server, ctx) => {
228
245
  },
229
246
  annotations: shared_js_1.IDEMPOTENT_WRITE,
230
247
  }, (0, shared_js_1.guarded)(async ({ workspace, request, collection, ...changes }) => {
231
- const ws = await ctx.resolveWorkspace(workspace);
232
- const { request: found, annotation } = await (0, lookups_js_1.resolveRequest)(ctx, ws.id, request, collection);
248
+ const { ws, value, note } = await (0, lookups_js_1.findRequest)(ctx, workspace, request, collection);
249
+ const { request: found, annotation } = value;
233
250
  const input = (0, requestShape_js_1.resolveMaskedValues)(changes, found);
234
251
  const folder = input.folder !== undefined ? await (0, lookups_js_1.resolveFolder)(ctx, ws.id, found.collectionId, input.folder) : undefined;
235
252
  const resolve = await (0, lookups_js_1.connectionResolverFor)(ctx, ws.id, (0, requestIntegrations_js_1.hasIntegrations)(input));
@@ -245,12 +262,13 @@ const registerRequestTools = (server, ctx) => {
245
262
  request: (0, requestShape_js_1.compactRequestView)(updated),
246
263
  changed: [...Object.keys(payload), ...(toggle ? ["isEnabledInRunner"] : [])],
247
264
  ...(warnings.length > 0 ? { warnings } : {}),
248
- ...(annotation ? { resolved: [{ input: request, matched: found.name, id: found.id, note: annotation }] } : {}),
265
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
266
+ ...(annotation || note ? { resolved: [{ input: request, matched: found.name, id: found.id, note: [annotation, note].filter(Boolean).join("; ") }] } : {}),
249
267
  });
250
268
  }));
251
269
  server.registerTool("move_requests", {
252
270
  title: "Move requests",
253
- description: "Move up to 200 requests (ids or unique names) into a collection and optional folder.",
271
+ description: "Move up to 200 requests (ids or unique names) into a collection and optional folder of the same workspace (the target collection's).",
254
272
  inputSchema: {
255
273
  workspace: shared_js_1.workspaceArg,
256
274
  requests: zod_1.z.array(zod_1.z.string().min(1).max(500)).min(1).max(MOVE_MAX),
@@ -260,8 +278,8 @@ const registerRequestTools = (server, ctx) => {
260
278
  },
261
279
  annotations: shared_js_1.IDEMPOTENT_WRITE,
262
280
  }, (0, shared_js_1.guarded)(async ({ workspace, requests, targetCollection, targetFolder, sourceCollection }) => {
263
- const ws = await ctx.resolveWorkspace(workspace);
264
- const target = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, targetCollection);
281
+ // Requests move within one workspace — the target collection's.
282
+ const { ws, value: target } = await (0, lookups_js_1.findCollection)(ctx, workspace, targetCollection);
265
283
  const folder = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, target.item.id, targetFolder);
266
284
  const moved = [];
267
285
  const failed = [];
@@ -294,24 +312,26 @@ const registerRequestTools = (server, ctx) => {
294
312
  annotations: shared_js_1.DESTRUCTIVE,
295
313
  }, (0, shared_js_1.guarded)(async ({ workspace, requests, collection, confirm }) => {
296
314
  (0, shared_js_1.requireConfirm)(confirm, "requests");
297
- const ws = await ctx.resolveWorkspace(workspace);
298
315
  const deleted = [];
299
316
  const failed = [];
300
317
  const touched = new Set();
301
318
  for (const ref of requests) {
302
319
  try {
303
- const { request } = await (0, lookups_js_1.resolveRequest)(ctx, ws.id, ref, collection, { exact: true });
320
+ const { ws, value } = await (0, lookups_js_1.findRequest)(ctx, workspace, ref, collection, { exact: true });
321
+ const { request } = value;
304
322
  await ctx.api.deleteRequest(ws.id, request.id);
305
- touched.add(request.collectionId);
306
- deleted.push({ id: request.id, name: request.name });
323
+ touched.add(`${ws.id}:${request.collectionId}`);
324
+ deleted.push({ id: request.id, name: request.name, workspace: (0, lookups_js_1.workspaceTag)(ws) });
307
325
  }
308
326
  catch (error) {
309
327
  failed.push({ request: ref, error: (0, shared_js_1.failureOf)(error) });
310
328
  }
311
329
  }
312
- for (const collectionId of touched)
313
- ctx.invalidate(`tree:${ws.id}:${collectionId}`);
314
- ctx.invalidate(`collections:${ws.id}`);
330
+ for (const key of touched) {
331
+ const [wsId, collectionId] = key.split(":");
332
+ ctx.invalidate(`tree:${wsId}:${collectionId}`);
333
+ ctx.invalidate(`collections:${wsId}`);
334
+ }
315
335
  return (0, shared_js_1.ok)({ deleted, failed });
316
336
  }));
317
337
  };
@@ -131,14 +131,16 @@ const registerRunTools = (server, ctx) => {
131
131
  workspace: shared_js_1.workspaceArg,
132
132
  request: (0, shared_js_1.refArg)("Request", "(name, \"METHOD url\" or id)"),
133
133
  collection: zod_1.z.string().max(200).optional().describe("Narrow a name lookup to one collection"),
134
- environment: zod_1.z.string().max(200).optional().describe("Environment name or id; omit to run without one"),
134
+ environment: zod_1.z.string().max(200).optional().describe("Environment name or id from the request's own workspace; omit to run without one"),
135
135
  dryRun: zod_1.z.boolean().optional(),
136
136
  maxBodyBytes: maxBodyArg,
137
137
  },
138
138
  annotations: shared_js_1.RUN,
139
139
  }, (0, shared_js_1.guarded)(async ({ workspace, request, collection, environment, dryRun, maxBodyBytes }) => {
140
- const ws = await ctx.resolveWorkspace(workspace);
141
- const { request: found, annotation } = await (0, lookups_js_1.resolveRequest)(ctx, ws.id, request, collection);
140
+ // The request decides the workspace; the environment must be one of its.
141
+ const { ws, value, note } = await (0, lookups_js_1.findRequest)(ctx, workspace, request, collection);
142
+ const { request: found } = value;
143
+ const annotation = [value.annotation, note].filter(Boolean).join("; ") || undefined;
142
144
  const prepared = await prepareRun(ctx, ws.id, found.collectionId, environment);
143
145
  const runnable = prepared.collection.requests.find((entry) => entry.id === found.id);
144
146
  if (!runnable)
@@ -179,6 +181,7 @@ const registerRunTools = (server, ctx) => {
179
181
  }));
180
182
  const summary = (0, summarize_js_1.summarizeRun)(outcome, maxBodyBytes ?? ctx.config.maxBodyBytes, {
181
183
  collection: { id: found.collectionId },
184
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
182
185
  environment: prepared.environment,
183
186
  warnings: [...prepared.warnings, ...skippedWarnings(prepared, [found.id])],
184
187
  ...(annotation ? { resolved: [{ input: request, matched: found.name, id: found.id, note: annotation }] } : {}),
@@ -199,8 +202,7 @@ const registerRunTools = (server, ctx) => {
199
202
  },
200
203
  annotations: shared_js_1.RUN,
201
204
  }, (0, shared_js_1.guarded)(async ({ workspace, collection, folder, only, environment, bail, maxBodyBytes }) => {
202
- const ws = await ctx.resolveWorkspace(workspace);
203
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, collection);
205
+ const { ws, value: resolved } = await (0, lookups_js_1.findCollection)(ctx, workspace, collection);
204
206
  const prepared = await prepareRun(ctx, ws.id, resolved.item.id, environment);
205
207
  let selection = null;
206
208
  if (folder) {
@@ -229,6 +231,7 @@ const registerRunTools = (server, ctx) => {
229
231
  }));
230
232
  return (0, shared_js_1.ok)((0, summarize_js_1.summarizeRun)(outcome, maxBodyBytes ?? ctx.config.maxBodyBytes, {
231
233
  collection: { id: resolved.item.id, name: resolved.item.name },
234
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
232
235
  environment: prepared.environment,
233
236
  warnings: [...prepared.warnings, ...skippedWarnings(prepared, outcome.results.map((result) => result.requestId))],
234
237
  resolved: [(0, resolve_js_1.resolvedSummary)(collection, resolved)],
@@ -47,15 +47,17 @@ const latestScenario = async (ctx, ws, id) => ctx.api.getScenario(ws, id);
47
47
  const registerScenarioTools = (server, ctx) => {
48
48
  server.registerTool("list_scenarios", {
49
49
  title: "List scenarios",
50
- description: "List API test scenarios (multi-step flows) with their folder, review status and version, plus the scenario folders and (on-prem) the groups you can create scenarios in. `query` filters by name. Scenarios are scoped by edition (on-prem: your groups), not strictly by workspace.",
50
+ description: "List API test scenarios (multi-step flows) you can access — regardless of workspace (on-prem: your groups) — with their folder, workspace, review status and version, plus the scenario folders and (on-prem) the groups you can create scenarios in. `query` filters by name.",
51
51
  inputSchema: { workspace: shared_js_1.workspaceArg, query: zod_1.z.string().max(200).optional(), folder: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
52
52
  annotations: shared_js_1.READ,
53
53
  }, (0, shared_js_1.guarded)(async ({ workspace, query, folder, cursor, limit }) => {
54
54
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
55
- const [{ scenarios, folders }, groups] = await Promise.all([
55
+ const [{ scenarios, folders }, groups, workspaces] = await Promise.all([
56
56
  (0, lookups_js_1.loadScenarioCatalog)(ctx, ws?.id ?? null),
57
57
  ctx.api.listMyScenarioGroups(ws?.id ?? null).catch(() => ({ items: [] })),
58
+ ctx.listWorkspaces(),
58
59
  ]);
60
+ const workspaceName = new Map(workspaces.map((entry) => [entry.id, entry.name]));
59
61
  const folderName = new Map(folders.map((entry) => [entry.id, entry.name]));
60
62
  const scope = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws?.id ?? null, folder) : null;
61
63
  const needle = query?.trim().toLowerCase();
@@ -67,7 +69,10 @@ const registerScenarioTools = (server, ctx) => {
67
69
  ...(ws ? { workspace: { id: ws.id, name: ws.name } } : {}),
68
70
  folders: folders.map((entry) => ({ id: entry.id, name: entry.name })),
69
71
  ...(groups.items.length > 0 ? { groups: groups.items.map((group) => ({ id: group.id, name: group.name, role: group.role })) } : {}),
70
- scenarios: page.items.map((scenario) => (0, summarize_js_1.scenarioSummaryView)(scenario, scenario.folderId ? (folderName.get(scenario.folderId) ?? null) : null)),
72
+ scenarios: page.items.map((scenario) => ({
73
+ ...(0, summarize_js_1.scenarioSummaryView)(scenario, scenario.folderId ? (folderName.get(scenario.folderId) ?? null) : null),
74
+ ...(workspaces.length > 1 && scenario.workspaceId ? { workspace: { id: scenario.workspaceId, name: workspaceName.get(scenario.workspaceId) ?? null } } : {}),
75
+ })),
71
76
  total: page.total,
72
77
  nextCursor: page.nextCursor,
73
78
  });
@@ -71,7 +71,7 @@ exports.workspaceArg = zod_1.z
71
71
  .string()
72
72
  .max(200)
73
73
  .optional()
74
- .describe("Workspace id or name. Defaults to CALLMAN_WORKSPACE_ID, or the only workspace you have; call list_workspaces to see them.");
74
+ .describe('Workspace id or name to narrow to. Omit it: lists/searches cover ALL your workspaces and names are found in whichever workspace has them; creating uses the session default (use_workspace). "all" = every workspace.');
75
75
  const refArg = (what, extra = "") => zod_1.z.string().min(1).max(500).describe(`${what} id or name${extra ? ` ${extra}` : ""}`);
76
76
  exports.refArg = refArg;
77
77
  exports.cursorArgs = {
@@ -9,7 +9,7 @@ const shared_js_1 = require("./shared.js");
9
9
  const registerWorkspaceTools = (server, ctx) => {
10
10
  server.registerTool("list_workspaces", {
11
11
  title: "List workspaces",
12
- description: "List the workspaces you belong to, with your role in each (owner / editor / viewer). A token keeps your role: writes are refused where you are a viewer.",
12
+ description: "List the workspaces you belong to, with your role in each (owner / editor / viewer) and which one is the session default. The token works in all of them; a token keeps your role: writes are refused where you are a viewer.",
13
13
  inputSchema: {},
14
14
  annotations: shared_js_1.READ,
15
15
  }, (0, shared_js_1.guarded)(async () => {
@@ -20,10 +20,26 @@ const registerWorkspaceTools = (server, ctx) => {
20
20
  name: workspace.name,
21
21
  role: workspace.role,
22
22
  memberCount: workspace.memberCount ?? null,
23
- isDefault: workspace.id === ctx.config.defaultWorkspaceId,
23
+ isDefault: workspace.id === ctx.getDefaultWorkspaceId(),
24
24
  })),
25
25
  });
26
26
  }));
27
+ server.registerTool("use_workspace", {
28
+ title: "Set the default workspace for this session",
29
+ description: "Pick the workspace new things go to for the rest of this session (collections, environments, scenarios, imports — anything created without `workspace`), and the one that wins when a name exists in several workspaces. It never hides the others: lists and searches still cover every workspace. `workspace: null` clears the default. Lasts until the MCP server restarts; CALLMAN_WORKSPACE_ID sets it at start.",
30
+ inputSchema: { workspace: zod_1.z.string().max(200).nullable().describe("Workspace name or id; null = no default") },
31
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
32
+ }, (0, shared_js_1.guarded)(async ({ workspace }) => {
33
+ const previous = ctx.getDefaultWorkspaceId();
34
+ if (workspace === null || !workspace.trim()) {
35
+ ctx.setDefaultWorkspaceId(null);
36
+ return (0, shared_js_1.ok)({ defaultWorkspace: null, previousDefaultId: previous, note: "No default workspace: creating something now needs `workspace`; reads still cover all workspaces." });
37
+ }
38
+ const target = await ctx.resolveWorkspace(workspace);
39
+ ctx.setDefaultWorkspaceId(target.id);
40
+ ctx.invalidate();
41
+ return (0, shared_js_1.ok)({ defaultWorkspace: { id: target.id, name: target.name, role: target.role }, previousDefaultId: previous });
42
+ }));
27
43
  server.registerTool("list_workspace_members", {
28
44
  title: "List workspace members",
29
45
  description: "List the members of a workspace with their role and status (active / invited).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callman-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Callman: lets Claude Code, Claude Desktop, Cursor, Codex and Gemini CLI manage collections, requests, environments, test scenarios and system-design diagrams in your Callman workspace — cloud or on-prem.",
5
5
  "keywords": [
6
6
  "callman",