callman-mcp 0.7.1 → 0.8.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 (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +22 -5
  3. package/dist/client/api.d.ts +59 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +67 -0
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/client/errors.d.ts.map +1 -1
  8. package/dist/client/errors.js +18 -11
  9. package/dist/client/errors.js.map +1 -1
  10. package/dist/client/types.d.ts +105 -0
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/context.d.ts +8 -0
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +1 -0
  15. package/dist/context.js.map +1 -1
  16. package/dist/domain/requestShape.d.ts +4 -4
  17. package/dist/resources/docs/mocks.d.ts +2 -0
  18. package/dist/resources/docs/mocks.d.ts.map +1 -0
  19. package/dist/resources/docs/mocks.js +210 -0
  20. package/dist/resources/docs/mocks.js.map +1 -0
  21. package/dist/resources/docs/overview.d.ts +1 -1
  22. package/dist/resources/docs/overview.d.ts.map +1 -1
  23. package/dist/resources/docs/overview.js +7 -1
  24. package/dist/resources/docs/overview.js.map +1 -1
  25. package/dist/resources/index.d.ts.map +1 -1
  26. package/dist/resources/index.js +2 -0
  27. package/dist/resources/index.js.map +1 -1
  28. package/dist/server.d.ts +5 -1
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +3 -2
  31. package/dist/server.js.map +1 -1
  32. package/dist/tools/catalog.d.ts +1 -1
  33. package/dist/tools/catalog.d.ts.map +1 -1
  34. package/dist/tools/catalog.js +18 -0
  35. package/dist/tools/catalog.js.map +1 -1
  36. package/dist/tools/index.d.ts.map +1 -1
  37. package/dist/tools/index.js +2 -0
  38. package/dist/tools/index.js.map +1 -1
  39. package/dist/tools/meta.d.ts.map +1 -1
  40. package/dist/tools/meta.js +29 -2
  41. package/dist/tools/meta.js.map +1 -1
  42. package/dist/tools/mockShared.d.ts +983 -0
  43. package/dist/tools/mockShared.d.ts.map +1 -0
  44. package/dist/tools/mockShared.js +387 -0
  45. package/dist/tools/mockShared.js.map +1 -0
  46. package/dist/tools/mocks.d.ts +4 -0
  47. package/dist/tools/mocks.d.ts.map +1 -0
  48. package/dist/tools/mocks.js +331 -0
  49. package/dist/tools/mocks.js.map +1 -0
  50. package/dist/tools/mocksLocal.d.ts +24 -0
  51. package/dist/tools/mocksLocal.d.ts.map +1 -0
  52. package/dist/tools/mocksLocal.js +169 -0
  53. package/dist/tools/mocksLocal.js.map +1 -0
  54. package/dist-cjs/client/api.js +67 -0
  55. package/dist-cjs/client/errors.js +18 -11
  56. package/dist-cjs/context.js +1 -0
  57. package/dist-cjs/resources/docs/mocks.js +212 -0
  58. package/dist-cjs/resources/docs/overview.js +7 -1
  59. package/dist-cjs/resources/index.js +2 -0
  60. package/dist-cjs/server.js +3 -2
  61. package/dist-cjs/tools/catalog.js +18 -0
  62. package/dist-cjs/tools/index.js +2 -0
  63. package/dist-cjs/tools/meta.js +28 -1
  64. package/dist-cjs/tools/mockShared.js +414 -0
  65. package/dist-cjs/tools/mocks.js +334 -0
  66. package/dist-cjs/tools/mocksLocal.js +179 -0
  67. package/package.json +1 -1
@@ -347,6 +347,73 @@ class CallmanApi {
347
347
  importUiTests(ws, body) {
348
348
  return this.http.request("POST", "/import/ui-tests", { workspaceId: ws, body, timeoutMs: 120_000 });
349
349
  }
350
+ // ── mock APIs (server) ──────────────────────────────────────────────
351
+ // Mocks are addressed by their slug (`mockId`), not the database id.
352
+ listMocks(ws, page = 1, search) {
353
+ return this.http.request("GET", "/mocks", { workspaceId: ws, query: { page, limit: 100, search } });
354
+ }
355
+ async listAllMocks(ws) {
356
+ const out = [];
357
+ for (let page = 1; page <= 20; page += 1) {
358
+ const result = await this.listMocks(ws, page);
359
+ out.push(...result.items);
360
+ if (page >= result.pagination.totalPages || result.items.length === 0)
361
+ break;
362
+ }
363
+ return out;
364
+ }
365
+ getMock(ws, mockId, includeSecrets = false) {
366
+ return this.http.request("GET", `/mocks/${encodeURIComponent(mockId)}`, { workspaceId: ws, query: includeSecrets ? { includeSecrets: "true" } : {} });
367
+ }
368
+ createMock(ws, body) {
369
+ return this.http.request("POST", "/mocks", { workspaceId: ws, body });
370
+ }
371
+ updateMock(ws, mockId, body) {
372
+ return this.http.request("PATCH", `/mocks/${encodeURIComponent(mockId)}`, { workspaceId: ws, body });
373
+ }
374
+ deleteMock(ws, mockId) {
375
+ return this.http.request("DELETE", `/mocks/${encodeURIComponent(mockId)}`, { workspaceId: ws });
376
+ }
377
+ addMockRoute(ws, mockId, body) {
378
+ return this.http.request("POST", `/mocks/${encodeURIComponent(mockId)}/routes`, { workspaceId: ws, body });
379
+ }
380
+ updateMockRoute(ws, mockId, routeId, body) {
381
+ return this.http.request("PATCH", `/mocks/${encodeURIComponent(mockId)}/routes/${encodeURIComponent(routeId)}`, { workspaceId: ws, body });
382
+ }
383
+ deleteMockRoute(ws, mockId, routeId) {
384
+ return this.http.request("DELETE", `/mocks/${encodeURIComponent(mockId)}/routes/${encodeURIComponent(routeId)}`, { workspaceId: ws });
385
+ }
386
+ reorderMockRoutes(ws, mockId, routeIds) {
387
+ return this.http.request("POST", `/mocks/${encodeURIComponent(mockId)}/routes/reorder`, { workspaceId: ws, body: { routeIds } });
388
+ }
389
+ getMockState(ws, mockId) {
390
+ return this.http.request("GET", `/mocks/${encodeURIComponent(mockId)}/state`, { workspaceId: ws });
391
+ }
392
+ replaceMockState(ws, mockId, state) {
393
+ return this.http.request("PUT", `/mocks/${encodeURIComponent(mockId)}/state`, { workspaceId: ws, body: state });
394
+ }
395
+ replaceMockInitialState(ws, mockId, initialState) {
396
+ return this.http.request("PUT", `/mocks/${encodeURIComponent(mockId)}/initial-state`, { workspaceId: ws, body: initialState });
397
+ }
398
+ resetMockState(ws, mockId) {
399
+ return this.http.request("POST", `/mocks/${encodeURIComponent(mockId)}/state/reset`, { workspaceId: ws, body: {} });
400
+ }
401
+ getMockStats(ws, mockId) {
402
+ return this.http.request("GET", `/mocks/${encodeURIComponent(mockId)}/stats`, { workspaceId: ws });
403
+ }
404
+ listMockLogs(ws, mockId, query) {
405
+ return this.http.request("GET", `/mocks/${encodeURIComponent(mockId)}/logs`, { workspaceId: ws, query });
406
+ }
407
+ // ── local mocks (the desktop app's own mock servers, via the relay) ─────
408
+ listLocalMockAgents(ws) {
409
+ return this.http.request("GET", "/local-mock-agents", { workspaceId: ws });
410
+ }
411
+ queueLocalMockCommand(ws, body) {
412
+ return this.http.request("POST", "/local-mock-commands", { workspaceId: ws, body });
413
+ }
414
+ getLocalMockCommand(ws, id) {
415
+ return this.http.request("GET", `/local-mock-commands/${encodeURIComponent(id)}`, { workspaceId: ws, retries: 0 });
416
+ }
350
417
  // ── local run bundle ────────────────────────────────────────────────
351
418
  pipelineBundle(ws, collectionId, environmentId) {
352
419
  return this.http.request("GET", "/pipeline/bundle", {
@@ -50,15 +50,17 @@ const SCOPE_HINT = "Create a token with that permission in the Callman desktop a
50
50
  const validationHint = (path) => {
51
51
  const doc = /\/(ui-test|import\/ui-tests|export\/ui-tests)/.test(path)
52
52
  ? "callman://docs/ui-tests (steps: callman://docs/ui-test-steps)"
53
- : /\/(scenarios|scenario-folders)/.test(path)
54
- ? "callman://docs/scenario-nodes"
55
- : /\/diagrams/.test(path)
56
- ? "callman://docs/diagram-schema"
57
- : /\/(contract-rules|data-contracts)/.test(path)
58
- ? "callman://docs/contracts"
59
- : /\/(environments|variables)/.test(path)
60
- ? null
61
- : "callman://docs/request-schema";
53
+ : /\/(mocks|local-mock-)/.test(path)
54
+ ? "callman://docs/mocks"
55
+ : /\/(scenarios|scenario-folders)/.test(path)
56
+ ? "callman://docs/scenario-nodes"
57
+ : /\/diagrams/.test(path)
58
+ ? "callman://docs/diagram-schema"
59
+ : /\/(contract-rules|data-contracts)/.test(path)
60
+ ? "callman://docs/contracts"
61
+ : /\/(environments|variables)/.test(path)
62
+ ? null
63
+ : "callman://docs/request-schema";
62
64
  return `The server rejected the input — check the fields listed in details${doc ? ` against ${doc}` : " (keys 1–100 chars, values ≤ 5000)"}.`;
63
65
  };
64
66
  const hintFor = (error) => {
@@ -68,12 +70,17 @@ const hintFor = (error) => {
68
70
  ? (error.details.required ?? []).join(" or ")
69
71
  : "an additional scope";
70
72
  const uiTests = /ui_tests:/.test(required) ? " UI tests have their own permissions: ui_tests:read (list, read, reports), ui_tests:write (create, edit, folders, schedules, review), ui_tests:run (run, cancel), ui_tests:delete." : "";
71
- return `The token lacks the permission ${required}.${uiTests} ${SCOPE_HINT}`;
73
+ const mocks = /mocks:/.test(required) ? " Mock APIs have their own permissions: mocks:read (list, read, logs, state), mocks:write (create, edit, routes, state, start/stop local mocks), mocks:delete." : "";
74
+ return `The token lacks the permission ${required}.${uiTests}${mocks} ${SCOPE_HINT}`;
72
75
  }
73
76
  case "PAT_ROUTE_FORBIDDEN":
74
77
  return /\/(ui-test|import\/ui-tests|export\/ui-tests)/.test(error.path)
75
78
  ? "This server does not open UI tests to AI tools yet — update callman-backend to a version with the ui_tests:* token permissions."
76
- : "This area of Callman is not available to AI tools (mocks, request history, API-scenario schedules, connection editing, admin). Use the desktop app for it.";
79
+ : /\/(mocks|local-mock-)/.test(error.path)
80
+ ? "This server does not open mock APIs to AI tools yet — update callman-backend to a version with the mocks:* token permissions."
81
+ : "This area of Callman is not available to AI tools (request history, API-scenario schedules, connection editing, admin). Use the desktop app for it.";
82
+ case "LOCAL_MOCK_DESKTOP_OFFLINE":
83
+ return "No Callman desktop app of yours is online to manage local mocks. Open the Callman desktop app, sign in, and turn on Settings → AI tools → \"Let AI tools manage local mock APIs on this computer\". Then try again — or use target: \"server\" for a mock on the Callman server with a public URL.";
77
84
  case "UI_TEST_DESKTOP_OFFLINE":
78
85
  return "No Callman desktop app of yours is online to run this. Open the Callman desktop app, sign in, and turn on Settings → AI tools → \"Let AI tools run UI tests on this computer\"; for Android/iOS connect a device or start an emulator/simulator. Then run again (list_ui_test_devices shows what is connected). Web flows can also run on the server runner when the installation has one.";
79
86
  case "UI_TEST_DESKTOP_PLATFORM_UNAVAILABLE":
@@ -93,6 +93,7 @@ const createToolContext = (options) => {
93
93
  },
94
94
  invalidate: (prefix) => (prefix ? cache.delete(prefix) : cache.clear()),
95
95
  pollDelaysMs: options.pollDelaysMs ?? { fast: 2000, slow: 5000 },
96
+ localMockPoll: options.localMockPoll ?? { intervalMs: 1000, timeoutMs: 25_000 },
96
97
  };
97
98
  };
98
99
  exports.createToolContext = createToolContext;
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MOCKS_DOC = void 0;
4
+ exports.MOCKS_DOC = `# Mock APIs through MCP
5
+
6
+ A mock API is a fake HTTP API that answers with the responses you describe. Use it when a user says
7
+ *"I need an API that returns …"* — to build a front end before the backend exists, to test a client
8
+ against errors and slow answers, or to give a test a stable dependency. You create it with a few tool
9
+ calls and hand the user a URL.
10
+
11
+ ## Two places a mock can live (\`target\`)
12
+
13
+ | | \`target: "server"\` (default) | \`target: "desktop"\` |
14
+ |---|---|---|
15
+ | Where | On the Callman server | Inside the user's Callman desktop app |
16
+ | URL | \`<server>/mock/<slug>/…\` — public, reachable by anyone with the URL (or the token) | \`http://127.0.0.1:<port>/…\` — this computer only |
17
+ | Responses | static, **template** ({{…}}), **builder** presets, **variants**, request validation, delay | **static only** + delay |
18
+ | State | yes (builder routes read and write it) | no |
19
+ | Lifetime | expires after 1 / 6 / 24 / 48 h (default 48) | until deleted; lives while the app is open |
20
+ | Needs | token with \`mocks:*\` | token with \`mocks:*\` **and** the desktop app open, signed in, with Settings → AI tools → "Let AI tools manage local mock APIs on this computer" on |
21
+ | Routes are edited with | add_mock_route / update_mock_route / delete_mock_route | \`update_mock { routes }\` — REPLACES all routes |
22
+ | Always on? | yes (no start/stop) | start_mock / stop_mock |
23
+
24
+ If the server's \`publicUrl\` does not answer from your machine (an installation that never set its public address announces a host nobody can reach), use \`alternativeUrl\` from the same result — it is the API address plus \`/mock/<slug>\`.
25
+
26
+ Pick **server** unless the user wants it on their own machine, needs no network exposure, or a tool on
27
+ their computer must call it at localhost. Desktop calls go through a relay: the app picks the request up
28
+ within a few seconds. If it answers \`LOCAL_MOCK_DESKTOP_OFFLINE\`, tell the user to open the app and
29
+ switch the setting on (or fall back to a server mock).
30
+
31
+ ## From a user's request to routes
32
+
33
+ 1. List first (\`list_mocks\`) — reuse or extend a mock that already fits instead of creating a duplicate.
34
+ 2. For each endpoint the user needs, decide: **method + path**, the **status code** and the **body**.
35
+ Use real-looking data; ask only when the contract is truly unknown.
36
+ 3. A fixed answer → a static response. Values taken from the request (the id in the path, a field of the
37
+ body) or random data → a template. A small CRUD API that remembers what was created → builder
38
+ routes over state. Different answers for different inputs (a 401 for a bad header, a 404 for one id)
39
+ → variants.
40
+ 4. Call \`create_mock\` with all routes at once. The result has the URL and a \`curl\` line per route —
41
+ give them to the user, and try one if you can.
42
+ 5. Iterate with \`update_mock_route\` / \`add_mock_route\`; read what real clients sent with \`get_mock_logs\`.
43
+
44
+ \`\`\`json
45
+ create_mock {
46
+ "name": "Orders API", "slug": "orders-api", "ttlHours": 24,
47
+ "routes": [
48
+ { "method": "GET", "path": "/orders", "statusCode": 200, "body": [{ "id": 1, "status": "NEW" }, { "id": 2, "status": "PAID" }] },
49
+ { "method": "GET", "path": "/orders/:id", "response": { "type": "template", "statusCode": 200, "body": { "id": "{{request.params.id}}", "status": "NEW", "createdAt": "{{now}}" } } },
50
+ { "method": "POST", "path": "/orders", "statusCode": 201, "body": { "id": "{{faker.uuid}}" }, "delay": { "mode": "fixed", "ms": 300 } }
51
+ ]
52
+ }
53
+ \`\`\`
54
+
55
+ ## A route
56
+
57
+ \`\`\`
58
+ { method, path, name?, response | (statusCode?, headers?, body?), variants?, delay?, requestValidation? }
59
+ \`\`\`
60
+
61
+ - **method**: GET POST PUT DELETE PATCH or ANY (any method that no specific route takes).
62
+ - **path**: starts with \`/\`; letters, digits and \`/ _ - : * .\`; no \`..\`; at most 256 characters.
63
+ \`:name\` is a path parameter (letters, digits, underscore: \`:userId\`, not \`:user-id\`). A trailing
64
+ \`/*\` (or a \`*\` segment) matches everything below. The most specific route wins (literal segment >
65
+ parameter > wildcard); a trailing slash is ignored. One route per method + path.
66
+ - **response**: the full shape below. **Shorthand** for the common case: \`statusCode\` (default 200),
67
+ \`headers\`, \`body\` directly on the route = a static response. Don't mix the two.
68
+ - **name**: a label (shown in lists; usable as a reference).
69
+ - **delay**: \`{ mode: "none" }\`, \`{ mode: "fixed", ms }\` or \`{ mode: "random", minMs, maxMs }\` — the
70
+ answer is held back that long (server ≤ 60 000 ms, desktop ≤ 30 000 ms). Use it to test timeouts and spinners.
71
+
72
+ ### Responses (server)
73
+
74
+ \`\`\`
75
+ { "type": "static", "statusCode": 200, "headers": { "X-Foo": "1" }, "body": <any JSON> }
76
+ { "type": "template", "statusCode": 200, "headers": {}, "body": <JSON with {{expressions}}> }
77
+ { "type": "builder", "preset": "list|detail|create|update|delete|echo|custom", "config": { "preset": <same>, … } }
78
+ \`\`\`
79
+
80
+ - **body**: any JSON value. An object/array is sent as JSON (\`content-type: application/json\` unless you
81
+ set one); a *string* is sent as is (\`text/plain\` unless you set a content type — for XML/HTML put
82
+ the text in \`body\` and set \`headers\`). No body = empty response (use for 204).
83
+ - **template** — every string in the body may contain \`{{expression}}\`. A string that is *only* one
84
+ expression keeps its type (number, boolean, object); inside longer text the value becomes text.
85
+ Unknown expressions become \`null\` (whole) or empty (inline).
86
+ - \`request.params.<name>\`, \`request.query.<name>\`, \`request.headers.<name>\` (case-insensitive),
87
+ \`request.body.<dotted.path>\`, \`request.method\`
88
+ - \`state.<key>\` — the mock's state (see below)
89
+ - \`faker.email|uuid|fullName|firstName|lastName|phone|address|company|lorem|boolean|int\`
90
+ - \`random.int(min,max)\`, \`random.float(min,max)\`, \`random.boolean\`
91
+ - \`now\`, \`now+5m\`, \`now-2d\` (units s m h d) → ISO timestamps
92
+ - Example: \`{ "id": "{{request.params.id}}", "email": "{{faker.email}}", "age": "{{random.int(18,65)}}" }\`
93
+ (\`age\` becomes a number).
94
+
95
+ ### Builder presets (server)
96
+
97
+ A builder route does the work of a REST endpoint over **state** (arrays of objects, stored per mock).
98
+ \`config.preset\` must equal \`preset\`. Required keys are marked; the rest have defaults.
99
+
100
+ | preset | config | what it does |
101
+ |---|---|---|
102
+ | \`list\` | **source**: \`{kind:"static",data:[…]}\` or \`{kind:"state",stateKey}\`; \`filtering:{enabled,rules:[{queryParam,field,operator:equals\\|contains\\|startsWith\\|in}]}\`; \`sorting:{enabled,sortByParam:"sortBy",orderParam:"order",allowedFields:[…]}\`; \`pagination:{enabled,strategy:page\\|offset\\|cursor,defaultPageSize:20,maxPageSize:100,envelope:standard\\|data-meta\\|raw}\`; \`statusCode\`, \`headers\` | filter by query params, sort, paginate (\`?page=&limit=\`, \`?offset=&limit=\`, \`?cursor=<last id>&limit=\`). Envelopes: standard \`{items,total,page,totalPages}\`, data-meta \`{data,meta:{total,page,limit}}\`, raw array |
103
+ | \`detail\` | **source**, **notFoundBehavior**:\`{statusCode:404,body?}\`, \`paramName:"id"\`, \`lookupField:"id"\` | the item whose \`lookupField\` equals the path parameter; otherwise the not-found answer |
104
+ | \`create\` | **stateKey**, \`autoGenerate:{id:"uuid"\\|"incremental",createdAt,updatedAt}\`, \`requiredFields:[…]\` (missing → 400), \`responseBehavior:return-created\\|return-id\\|status-only\`, \`statusCode:201\` | appends the request body (plus generated fields) to \`state[stateKey]\` |
105
+ | \`update\` | **stateKey**, **notFoundBehavior**, \`paramName\`, \`lookupField\`, \`mergeStrategy:partial\\|full\`, \`autoUpdateUpdatedAt:true\`, \`statusCode:200\` | replaces/merges the matching item with the request body |
106
+ | \`delete\` | **stateKey**, **notFoundBehavior**, \`paramName\`, \`lookupField\`, \`responseBehavior:204\\|return-deleted\\|success-message\`, \`statusCode:204\` | removes the matching item |
107
+ | \`echo\` | \`statusCode\`, \`headers\` | answers with the request: \`{method,params,query,headers,body}\` — handy to see what a client sends |
108
+ | \`custom\` | \`statusCode\`, \`headers\`, \`body\` | a fixed answer through the builder |
109
+
110
+ A full CRUD mock over one \`users\` collection (the path parameter name must match \`paramName\`):
111
+
112
+ \`\`\`json
113
+ create_mock { "name": "Users", "slug": "users-api", "routes": [
114
+ { "method": "GET", "path": "/users", "response": { "type": "builder", "preset": "list", "config": { "preset": "list", "source": { "kind": "state", "stateKey": "users" }, "pagination": { "enabled": true } } } },
115
+ { "method": "GET", "path": "/users/:id", "response": { "type": "builder", "preset": "detail", "config": { "preset": "detail", "source": { "kind": "state", "stateKey": "users" }, "notFoundBehavior": { "statusCode": 404 } } } },
116
+ { "method": "POST", "path": "/users", "response": { "type": "builder", "preset": "create", "config": { "preset": "create", "stateKey": "users", "autoGenerate": { "id": "incremental", "createdAt": true }, "requiredFields": ["name"] } } },
117
+ { "method": "PATCH", "path": "/users/:id", "response": { "type": "builder", "preset": "update", "config": { "preset": "update", "stateKey": "users", "notFoundBehavior": { "statusCode": 404 } } } },
118
+ { "method": "DELETE", "path": "/users/:id", "response": { "type": "builder", "preset": "delete", "config": { "preset": "delete", "stateKey": "users", "notFoundBehavior": { "statusCode": 404 } } } }
119
+ ] }
120
+ set_mock_state { "mock": "users-api", "initialState": { "users": [{ "id": 1, "name": "Ada" }] }, "state": { "users": [{ "id": 1, "name": "Ada" }] } }
121
+ \`\`\`
122
+
123
+ ### State
124
+
125
+ State is a JSON object per mock (limit 1 MB). Builder \`create\`/\`update\`/\`delete\` change it as requests
126
+ arrive (a missing array is created by the first \`create\`); \`list\`/\`detail\` with \`source.kind: "state"\` and
127
+ \`{{state.key}}\` templates read it. \`get_mock_state\` shows \`state\` (live) and \`initialState\`;
128
+ \`set_mock_state\` replaces either wholesale; \`reset_mock_state\` copies \`initialState\` back into \`state\`
129
+ (do that between test runs). Seed *both* when you want a reset to return to the seed data.
130
+
131
+ ### Variants — different answers for different requests (server)
132
+
133
+ A route can carry up to 20 variants. Each has \`priority\` (lower is checked first), a \`condition\` and a
134
+ \`response\` (same shapes as above). The first variant whose condition matches answers; otherwise the
135
+ route's own \`response\` does.
136
+
137
+ \`\`\`json
138
+ { "method": "GET", "path": "/profile",
139
+ "response": { "type": "static", "statusCode": 200, "body": { "name": "Ada" } },
140
+ "variants": [
141
+ { "priority": 1, "condition": { "type": "all", "rules": [{ "field": "headers.authorization", "operator": "not-exists" }] },
142
+ "response": { "type": "static", "statusCode": 401, "body": { "error": "unauthorized" } } },
143
+ { "priority": 2, "condition": { "type": "any", "rules": [{ "field": "query.fail", "operator": "equals", "value": "1" }] },
144
+ "response": { "type": "static", "statusCode": 500, "body": { "error": "boom" } } } ] }
145
+ \`\`\`
146
+
147
+ - \`condition.type\`: \`all\` (every rule) or \`any\` (at least one); 1–20 rules.
148
+ - \`field\`: \`headers.<name>\`, \`params.<name>\`, \`query.<name>\`, \`body.<dotted.path>\`.
149
+ - \`operator\`: exists, not-exists (null and empty count as missing), equals, not-equals, contains,
150
+ starts-with, matches (a regular expression in \`value\`; an invalid one never matches). Values are compared as text.
151
+
152
+ ### Request validation (server)
153
+
154
+ \`requestValidation: { "mode": "schema", "schema": { …JSON Schema draft-07… } }\` makes the route reject a
155
+ body that does not match with **400** and the list of invalid fields — to let a client team verify what
156
+ they send. Checked on POST, PUT and PATCH requests. \`mode: "any"\` (default) accepts anything. An invalid
157
+ schema is refused when you save the route. \`update_mock_route\` with \`requestValidation: null\` removes the gate.
158
+
159
+ ### Auth (server)
160
+
161
+ \`auth: "public"\` (default) — anyone with the URL. \`auth: "token"\` — callers must send
162
+ \`X-Mock-Token: <token>\` (else 401). The token is generated by the server and returned **once**, in the
163
+ create_mock / update_mock result; \`get_mock\` keeps it masked unless the token holds \`environments:reveal\`
164
+ and you pass \`reveal: true\`. Never put it into places the user did not ask for.
165
+
166
+ ## Limits (server defaults; an installation may change them)
167
+
168
+ 20 mocks per workspace · 50 routes per mock · response body ≤ 100 KB · request body ≤ 1 MB ·
169
+ state ≤ 1 MB · 20 000 requests in total per mock, then 429 · 30 requests per second per mock, then 429 ·
170
+ the 500 newest request logs are kept · slugs: 3–48 chars, lowercase letters/digits/single dashes, start
171
+ with a letter, a few reserved words (api, mock, admin …) refused · lifetime 1/6/24/48 h, then **410 Gone**.
172
+ Local (desktop): response body ≤ 256 KB, ports 1024–65535 (omit \`port\` and the app picks a free one),
173
+ the mock listens on 127.0.0.1 only.
174
+
175
+ ## Desktop (local) mocks
176
+
177
+ \`create_mock { target: "desktop", name, port?, autoStart?, routes? }\` creates a local mock in the user's
178
+ app and starts it (\`start: false\` to skip). Routes are static: \`{ method, path, name?, statusCode, headers, body, delay }\`
179
+ (or \`response: { type: "static", … }\` — the same route object you would give a server mock works as
180
+ long as it is static; \`variants\`, \`requestValidation\`, template and builder responses are refused
181
+ with LOCAL_MOCK_FEATURE_UNSUPPORTED). \`body\` is text: a string is sent verbatim, anything else is
182
+ sent as formatted JSON. \`:param\` segments and a trailing \`/*\` work; every request is logged
183
+ (\`get_mock_logs { target: "desktop" }\`, the latest requests only, kept until the app quits).
184
+ To change routes: \`get_mock { target: "desktop" }\`, edit, \`update_mock { routes: [...all of them] }\`.
185
+ \`start_mock\` / \`stop_mock\` / \`delete_mock\` act on one by name or id. Mocks the AI created carry an "AI"
186
+ badge in the app. The mock is only reachable from the user's computer — don't hand out the URL to others.
187
+
188
+ ## Tools
189
+
190
+ | Tool | Scope | |
191
+ |---|---|---|
192
+ | list_mocks, get_mock, get_mock_logs | mocks:read | \`target\` server/desktop |
193
+ | get_mock_stats, get_mock_state | mocks:read | server |
194
+ | create_mock, update_mock | mocks:write | \`target\` server/desktop |
195
+ | add_mock_route, update_mock_route, reorder_mock_routes, set_mock_state, reset_mock_state | mocks:write | server |
196
+ | start_mock, stop_mock | mocks:write | desktop |
197
+ | delete_mock | mocks:delete (+ \`confirm: true\`) | \`target\` server/desktop |
198
+ | delete_mock_route | mocks:delete (+ \`confirm: true\`) | server |
199
+
200
+ \`mock\` is the name, the slug or the id. A route is referenced by routeId, \`"GET /users/:id"\`, its path
201
+ (when unique) or its name. If \`create_mock\` fails on a route, the new mock is deleted again (needs
202
+ mocks:delete; otherwise the result says it is kept) and the error names the failing route and why.
203
+
204
+ ## Errors you may meet
205
+
206
+ \`PAT_SCOPE_MISSING\` (token lacks mocks:read/write/delete) · \`FEATURE_DISABLED\` (mocks switched off for the
207
+ account) · \`MOCK_SLUG_TAKEN\` (pick another slug) · \`MOCK_LIMIT_REACHED\` (delete an old mock) ·
208
+ \`ROUTE_PATH_CONFLICT\` (the method + path exists) · \`MOCK_ROUTE_LIMIT_REACHED\` · \`VALIDATION_ERROR\`
209
+ (details name the field) · \`LOCAL_MOCK_DESKTOP_OFFLINE\` (open the desktop app and switch the setting on) ·
210
+ \`LOCAL_MOCK_TIMEOUT\` (the app did not answer; check with list_mocks before repeating a create) ·
211
+ \`LOCAL_MOCK_FEATURE_UNSUPPORTED\` (use a server mock).
212
+ `;
@@ -4,7 +4,7 @@ exports.OVERVIEW_DOC = void 0;
4
4
  exports.OVERVIEW_DOC = `# Working with Callman through MCP
5
5
 
6
6
  Callman is an API client and test platform (collections of HTTP requests, environments,
7
- multi-step API test scenarios, UI tests for web / Android / iOS, system-design diagrams). This
7
+ multi-step API test scenarios, UI tests for web / Android / iOS, system-design diagrams, mock APIs). This
8
8
  server exposes it to you through tools; everything you do goes to the same server the Callman desktop app uses, as the user
9
9
  whose token is configured, limited to that token's permissions and the user's workspace role.
10
10
 
@@ -74,6 +74,12 @@ web without a runner) run on the user's own Callman desktop app, which must be o
74
74
  "Let AI tools run UI tests on this computer" on — \`list_ui_test_devices\` shows what is
75
75
  available. Runs return per-step results; screenshots come back as images on request.
76
76
 
77
+ ## Mock APIs
78
+ When a user needs an API that does not exist (yet), create a mock: \`create_mock\` with its routes gives a
79
+ public URL (\`target: "server"\`, default) or a local one on the user's computer (\`target: "desktop"\`, needs
80
+ the desktop app open with "Let AI tools manage local mock APIs on this computer" on). Responses can be
81
+ static, templated or backed by state; read \`callman://docs/mocks\` first. Server mocks expire (1–48 h).
82
+
77
83
  ## Diagrams
78
84
  System-design diagrams use a fixed component catalog — \`callman://diagram/components\`.
79
85
  Build with \`create_diagram\` (auto layout), edit with \`patch_diagram\`, share with
@@ -11,6 +11,7 @@ const lookups_js_1 = require("../tools/lookups.js");
11
11
  const uiTestShared_js_1 = require("../tools/uiTestShared.js");
12
12
  const contracts_js_1 = require("./docs/contracts.js");
13
13
  const diagramSchema_js_1 = require("./docs/diagramSchema.js");
14
+ const mocks_js_1 = require("./docs/mocks.js");
14
15
  const overview_js_1 = require("./docs/overview.js");
15
16
  const requestSchema_js_1 = require("./docs/requestSchema.js");
16
17
  const collectionScripts_js_1 = require("./docs/collectionScripts.js");
@@ -49,6 +50,7 @@ const registerResources = (server, ctx) => {
49
50
  { name: "docs-scripting-sandbox", uri: "callman://docs/scripting/sandbox", title: "Scripting: sandbox and feature policy", description: "Globals available in the script sandbox and the feature policy for DB/Kafka capabilities.", body: () => `${aiPacks_js_1.AI_PACKS["sandbox-globals"] ?? ""}\n\n${aiPacks_js_1.AI_PACKS["feature-policy"] ?? ""}` },
50
51
  { name: "docs-ui-tests", uri: "callman://docs/ui-tests", title: "UI tests", description: "UI-test concepts: platforms, header, preconditions and sub-flows, datasets and DB sources, hybrid data steps, settings, where runs happen (server runner vs your desktop app), reports and screenshots, schedules, review, masked secrets, and the recommended workflow.", body: () => uiTests_js_1.UI_TESTS_DOC },
51
52
  { name: "docs-ui-test-steps", uri: "callman://docs/ui-test-steps", title: "UI-test step reference", description: "Every UI-test step (web / Android / iOS / hybrid data steps): aliases, platforms, forms, arguments and an example — generated from callman-core, the same catalog the linter uses.", body: () => (0, callman_core_1.renderUiTestCommandReference)() },
53
+ { name: "docs-mocks", uri: "callman://docs/mocks", title: "Mock APIs", description: "Mock APIs from an AI tool: server mocks (public URL; static, template and builder responses, variants, request validation, delay, state, auth) vs local mocks on the user's desktop app (static, 127.0.0.1), how to turn a user's request into routes, limits, errors.", body: () => mocks_js_1.MOCKS_DOC },
52
54
  { name: "docs-diagram-schema", uri: "callman://docs/diagram-schema", title: "Diagram schema", description: "Every node and edge field, styling (colors, borders, fonts), containers, edge semantics and colors, patch operations and merge rules, and the stored DiagramDocument.", body: () => diagramSchema_js_1.DIAGRAM_SCHEMA_DOC },
53
55
  ];
54
56
  for (const doc of staticDocs) {
@@ -7,8 +7,8 @@ const index_js_1 = require("./prompts/index.js");
7
7
  const index_js_2 = require("./resources/index.js");
8
8
  const index_js_3 = require("./tools/index.js");
9
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 API test scenarios, UI tests (web / Android / iOS flows), 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. The token works in every workspace the user belongs to: lists and searches cover all of them unless you pass \`workspace\`, names are looked up in the default workspace first and then everywhere, and results say which workspace they come from; creating something needs a target (\`workspace\`, the collection it goes into, or the session default — use_workspace). Names work as references (collection, folder, request, environment, scenario, UI test, 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. Arguments are strict — an unknown key is an error listing the valid ones. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating or editing requests, callman://docs/contracts before contract checks, callman://docs/scenario-nodes before scenarios, callman://docs/ui-tests (+ callman://docs/ui-test-steps) before UI tests — they run on the server runner (web) or the user's own desktop app (list_ui_test_devices), and callman://docs/diagram-schema + callman://diagram/components before diagrams.`;
10
+ exports.SERVER_INSTRUCTIONS = `Callman is an API client and test platform: collections of HTTP requests, environments with variables, multi-step API test scenarios, UI tests (web / Android / iOS flows), mock APIs, 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. The token works in every workspace the user belongs to: lists and searches cover all of them unless you pass \`workspace\`, names are looked up in the default workspace first and then everywhere, and results say which workspace they come from; creating something needs a target (\`workspace\`, the collection it goes into, or the session default — use_workspace). Names work as references (collection, folder, request, environment, scenario, UI test, 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. Arguments are strict — an unknown key is an error listing the valid ones. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating or editing requests, callman://docs/contracts before contract checks, callman://docs/scenario-nodes before scenarios, callman://docs/ui-tests (+ callman://docs/ui-test-steps) before UI tests — they run on the server runner (web) or the user's own desktop app (list_ui_test_devices), callman://docs/mocks before mock APIs (create_mock: a public URL on the server, or target: "desktop" for a local mock in the user's desktop app), and callman://docs/diagram-schema + callman://diagram/components before diagrams.`;
12
12
  const createCallmanMcpServer = (options) => {
13
13
  const version = options.version ?? "0.0.0";
14
14
  const context = (0, context_js_1.createToolContext)({
@@ -18,6 +18,7 @@ const createCallmanMcpServer = (options) => {
18
18
  logger: options.logger,
19
19
  cacheTtlMs: options.cacheTtlMs,
20
20
  pollDelaysMs: options.pollDelaysMs,
21
+ localMockPoll: options.localMockPoll,
21
22
  });
22
23
  const server = new mcp_js_1.McpServer({ name: exports.SERVER_NAME, version, title: "Callman" }, { capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: exports.SERVER_INSTRUCTIONS });
23
24
  const tools = (0, index_js_3.registerAllTools)(server, context);
@@ -99,6 +99,24 @@ exports.TOOL_CATALOG = [
99
99
  { name: "restore_ui_test_version", group: "uiTests", scopes: ["ui_tests:write"] },
100
100
  { name: "export_ui_tests", group: "uiTests", scopes: ["ui_tests:read"] },
101
101
  { name: "import_ui_tests", group: "uiTests", scopes: ["ui_tests:write"] },
102
+ // Mock APIs: mocks:* scopes. Server mocks (/api/mocks) and, with
103
+ // target: "desktop", the local mocks of the user's own desktop app.
104
+ { name: "list_mocks", group: "mocks", scopes: ["mocks:read"] },
105
+ { name: "get_mock", group: "mocks", scopes: ["mocks:read"] },
106
+ { name: "get_mock_logs", group: "mocks", scopes: ["mocks:read"] },
107
+ { name: "get_mock_stats", group: "mocks", scopes: ["mocks:read"] },
108
+ { name: "get_mock_state", group: "mocks", scopes: ["mocks:read"] },
109
+ { name: "create_mock", group: "mocks", scopes: ["mocks:write"] },
110
+ { name: "update_mock", group: "mocks", scopes: ["mocks:write"] },
111
+ { name: "add_mock_route", group: "mocks", scopes: ["mocks:write"] },
112
+ { name: "update_mock_route", group: "mocks", scopes: ["mocks:write"] },
113
+ { name: "reorder_mock_routes", group: "mocks", scopes: ["mocks:write"] },
114
+ { name: "set_mock_state", group: "mocks", scopes: ["mocks:write"] },
115
+ { name: "reset_mock_state", group: "mocks", scopes: ["mocks:write"] },
116
+ { name: "start_mock", group: "mocks", scopes: ["mocks:write"] },
117
+ { name: "stop_mock", group: "mocks", scopes: ["mocks:write"] },
118
+ { name: "delete_mock", group: "mocks", scopes: ["mocks:delete"], destructive: true },
119
+ { name: "delete_mock_route", group: "mocks", scopes: ["mocks:delete"], destructive: true },
102
120
  { name: "list_diagrams", group: "diagrams", scopes: ["diagrams:read"] },
103
121
  { name: "get_diagram", group: "diagrams", scopes: ["diagrams:read"] },
104
122
  { name: "create_diagram", group: "diagrams", scopes: ["diagrams:write"] },
@@ -9,6 +9,7 @@ const environments_js_1 = require("./environments.js");
9
9
  const import_js_1 = require("./import.js");
10
10
  const loadTest_js_1 = require("./loadTest.js");
11
11
  const meta_js_1 = require("./meta.js");
12
+ const mocks_js_1 = require("./mocks.js");
12
13
  const requests_js_1 = require("./requests.js");
13
14
  const run_js_1 = require("./run.js");
14
15
  const scenarios_js_1 = require("./scenarios.js");
@@ -56,6 +57,7 @@ const registerAllTools = (host, ctx) => {
56
57
  (0, uiTests_js_1.registerUiTestTools)(server, ctx);
57
58
  (0, uiTestRuns_js_1.registerUiTestRunTools)(server, ctx);
58
59
  (0, uiTestAdmin_js_1.registerUiTestAdminTools)(server, ctx);
60
+ (0, mocks_js_1.registerMockTools)(server, ctx);
59
61
  (0, diagrams_js_1.registerDiagramTools)(server, ctx);
60
62
  if (ctx.config.allowLocalRun) {
61
63
  (0, run_js_1.registerRunTools)(server, ctx);
@@ -8,6 +8,30 @@ const shared_js_1 = require("./shared.js");
8
8
  // it who it acts as, which workspaces exist, which features are on and which
9
9
  // tools the token can actually use. Never throws for a missing/invalid token:
10
10
  // the answer explains how to fix the setup.
11
+ // Mock APIs at a glance: is the feature on, which of read / write / delete the
12
+ // token holds, and whether a desktop app is online for target: "desktop".
13
+ const mocksOverview = async (ctx, scopes, features, workspaceId) => {
14
+ const held = new Set(scopes);
15
+ const mine = catalog_js_1.TOOL_CATALOG.filter((entry) => entry.group === "mocks");
16
+ const usable = mine.filter((entry) => entry.scopes.every((scope) => held.has(scope)));
17
+ let desktopOnline = null;
18
+ if (held.has("mocks:read") && features.mocks !== false) {
19
+ try {
20
+ desktopOnline = (await ctx.api.listLocalMockAgents(workspaceId)).agents.length > 0;
21
+ }
22
+ catch {
23
+ desktopOnline = null;
24
+ }
25
+ }
26
+ return {
27
+ featureEnabled: features.mocks ?? null,
28
+ scopes: { read: held.has("mocks:read"), write: held.has("mocks:write"), delete: held.has("mocks:delete") },
29
+ toolsAvailable: usable.length,
30
+ toolsTotal: mine.length,
31
+ desktopAppOnline: desktopOnline,
32
+ ...(held.has("mocks:read") ? { desktopHint: 'target: "desktop" (local mocks on the user\'s computer) needs the Callman desktop app open and signed in, with Settings → AI tools → "Let AI tools manage local mock APIs on this computer" on. Read callman://docs/mocks.' } : {}),
33
+ };
34
+ };
11
35
  const registerMetaTools = (server, ctx) => {
12
36
  server.registerTool("whoami", {
13
37
  title: "Who am I (connection check)",
@@ -52,6 +76,8 @@ const registerMetaTools = (server, ctx) => {
52
76
  : workspaces.length === 1
53
77
  ? workspaces[0]
54
78
  : null;
79
+ const tools = (0, catalog_js_1.toolAvailability)(me.token.scopes, ctx.config.allowLocalRun);
80
+ const mocks = await mocksOverview(ctx, me.token.scopes, features, defaultWorkspace?.id ?? workspaces[0]?.id ?? null);
55
81
  return (0, shared_js_1.ok)({
56
82
  apiUrl: ctx.config.apiUrl,
57
83
  connected: true,
@@ -79,7 +105,8 @@ const registerMetaTools = (server, ctx) => {
79
105
  : {}),
80
106
  features,
81
107
  localRun: ctx.config.allowLocalRun,
82
- tools: (0, catalog_js_1.toolAvailability)(me.token.scopes, ctx.config.allowLocalRun),
108
+ mocks,
109
+ tools,
83
110
  });
84
111
  }));
85
112
  };