loopctl-mcp-server 1.0.1 → 1.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.
Files changed (3) hide show
  1. package/README.md +15 -5
  2. package/index.js +262 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP (Model Context Protocol) server for [loopctl](https://loopctl.com) -- structural trust for AI development loops.
4
4
 
5
- Wraps the loopctl REST API into 19 typed MCP tools so AI coding agents (Claude Code, etc.) can interact with loopctl without writing curl commands.
5
+ Wraps the loopctl REST API into 24 typed MCP tools so AI coding agents (Claude Code, etc.) can interact with loopctl without writing curl commands.
6
6
 
7
7
  ## Installation
8
8
 
@@ -65,7 +65,7 @@ Or if installed locally:
65
65
 
66
66
  Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
67
67
 
68
- ## Tools (19)
68
+ ## Tools (24)
69
69
 
70
70
  ### Project Tools
71
71
 
@@ -74,14 +74,14 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
74
74
  | `get_tenant` | Get current tenant info. Use to verify connectivity. |
75
75
  | `list_projects` | List all projects in the current tenant. |
76
76
  | `create_project` | Create a new project in the current tenant. |
77
- | `get_progress` | Get progress summary for a project, including story counts by status. |
77
+ | `get_progress` | Get progress summary for a project, including story counts by status. Pass `include_cost=true` for cost data. |
78
78
  | `import_stories` | Import stories into a project from a structured payload (Epic 12 import format). |
79
79
 
80
80
  ### Story Tools
81
81
 
82
82
  | Tool | Description |
83
83
  |---|---|
84
- | `list_stories` | List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. |
84
+ | `list_stories` | List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. Pass `include_token_totals=true` for per-story token data. |
85
85
  | `list_ready_stories` | List stories that are ready to be worked on (contracted, dependencies met). |
86
86
  | `get_story` | Get full details for a single story by ID. |
87
87
 
@@ -98,7 +98,7 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
98
98
 
99
99
  | Tool | Description |
100
100
  |---|---|
101
- | `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. |
101
+ | `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. |
102
102
  | `review_complete` | Record that a review has been completed for a story. Required before verify. |
103
103
 
104
104
  ### Verification Tools (orchestrator key)
@@ -115,6 +115,16 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
115
115
  | `bulk_mark_complete` | Bulk mark multiple stories as complete in a single API call. |
116
116
  | `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. |
117
117
 
118
+ ### Token Efficiency Tools
119
+
120
+ | Tool | Auth Key | Description |
121
+ |---|---|---|
122
+ | `report_token_usage` | agent | Report input/output token counts, model name, and cost for a story session. Calls `POST /api/v1/token-usage`. |
123
+ | `get_cost_summary` | orch | Get cost/token usage summary for a project, optionally broken down by `agent`, `epic`, or `model`. |
124
+ | `get_story_token_usage` | orch | Get all token usage records for a single story. |
125
+ | `get_cost_anomalies` | orch | Get cost anomaly alerts — stories or agents exceeding expected budgets. Optionally filter by project. |
126
+ | `set_token_budget` | orch | Set a token budget (in millicents) for a project, epic, story, or agent scope. Requires orchestrator role. |
127
+
118
128
  ### Discovery Tools
119
129
 
120
130
  | Tool | Description |
package/index.js CHANGED
@@ -115,6 +115,32 @@ function toContent(result) {
115
115
  };
116
116
  }
117
117
 
118
+ /**
119
+ * Compact variant for list endpoints — strips bulky fields (acceptance_criteria,
120
+ * description) from each story to stay within token limits. Callers can use
121
+ * get_story for full details on individual stories.
122
+ */
123
+ function toContentCompact(result) {
124
+ if (result && result.error === true) return toContent(result);
125
+
126
+ if (result && Array.isArray(result.data)) {
127
+ const compact = {
128
+ ...result,
129
+ data: result.data.map(({ acceptance_criteria, description, ...rest }) => rest),
130
+ };
131
+ return {
132
+ content: [
133
+ {
134
+ type: "text",
135
+ text: JSON.stringify(compact, null, 2),
136
+ },
137
+ ],
138
+ };
139
+ }
140
+
141
+ return toContent(result);
142
+ }
143
+
118
144
  // ---------------------------------------------------------------------------
119
145
  // Tool implementations
120
146
  // ---------------------------------------------------------------------------
@@ -140,8 +166,11 @@ async function createProject({ name, slug, repo_url, description, tech_stack })
140
166
  return toContent(result);
141
167
  }
142
168
 
143
- async function getProgress({ project_id }) {
144
- const result = await apiCall("GET", `/api/v1/projects/${project_id}/progress`);
169
+ async function getProgress({ project_id, include_cost }) {
170
+ const params = new URLSearchParams();
171
+ if (include_cost) params.set("include_cost", "true");
172
+ const query = params.toString() ? `?${params}` : "";
173
+ const result = await apiCall("GET", `/api/v1/projects/${project_id}/progress${query}`);
145
174
  return toContent(result);
146
175
  }
147
176
 
@@ -157,24 +186,25 @@ async function importStories({ project_id, payload }) {
157
186
 
158
187
  // --- Story Tools ---
159
188
 
160
- async function listStories({ project_id, agent_status, verified_status, epic_id, limit, offset }) {
189
+ async function listStories({ project_id, agent_status, verified_status, epic_id, limit, offset, include_token_totals }) {
161
190
  const params = new URLSearchParams({ project_id });
162
191
  if (agent_status) params.set("agent_status", agent_status);
163
192
  if (verified_status) params.set("verified_status", verified_status);
164
193
  if (epic_id) params.set("epic_id", epic_id);
165
- if (limit != null) params.set("limit", String(limit));
194
+ params.set("limit", String(limit ?? 20));
166
195
  if (offset != null) params.set("offset", String(offset));
196
+ if (include_token_totals) params.set("include_token_totals", "true");
167
197
 
168
198
  const result = await apiCall("GET", `/api/v1/stories?${params}`);
169
- return toContent(result);
199
+ return toContentCompact(result);
170
200
  }
171
201
 
172
202
  async function listReadyStories({ project_id, limit }) {
173
203
  const params = new URLSearchParams({ project_id });
174
- if (limit != null) params.set("limit", String(limit));
204
+ params.set("limit", String(limit ?? 20));
175
205
 
176
206
  const result = await apiCall("GET", `/api/v1/stories/ready?${params}`);
177
- return toContent(result);
207
+ return toContentCompact(result);
178
208
  }
179
209
 
180
210
  async function getStory({ story_id }) {
@@ -226,13 +256,16 @@ async function requestReview({ story_id }) {
226
256
 
227
257
  // --- Reviewer Tools (orch key — reviewer uses orchestrator role) ---
228
258
 
229
- async function reportStory({ story_id, artifact_type, artifact_path }) {
259
+ async function reportStory({ story_id, artifact_type, artifact_path, token_usage }) {
230
260
  const body = {};
231
261
  if (artifact_type || artifact_path) {
232
262
  body.artifact = {};
233
263
  if (artifact_type) body.artifact.artifact_type = artifact_type;
234
264
  if (artifact_path) body.artifact.path = artifact_path;
235
265
  }
266
+ if (token_usage) {
267
+ body.token_usage = token_usage;
268
+ }
236
269
 
237
270
  const result = await apiCall(
238
271
  "POST",
@@ -308,6 +341,58 @@ async function verifyAllInEpic({ epic_id, review_type, summary }) {
308
341
  return toContent(result);
309
342
  }
310
343
 
344
+ // --- Token Efficiency Tools ---
345
+
346
+ async function reportTokenUsage({ story_id, input_tokens, output_tokens, model_name, cost_millicents, phase, skill_version_id, session_id }) {
347
+ const body = { story_id, input_tokens, output_tokens, model_name, cost_millicents };
348
+ if (phase) body.phase = phase;
349
+ if (skill_version_id) body.skill_version_id = skill_version_id;
350
+ if (session_id) body.session_id = session_id;
351
+
352
+ const result = await apiCall(
353
+ "POST",
354
+ "/api/v1/token-usage",
355
+ body,
356
+ process.env.LOOPCTL_AGENT_KEY
357
+ );
358
+ return toContent(result);
359
+ }
360
+
361
+ async function getCostSummary({ project_id, breakdown }) {
362
+ const params = new URLSearchParams({ project_id });
363
+ if (breakdown) params.set("breakdown", breakdown);
364
+
365
+ const result = await apiCall("GET", `/api/v1/projects/${project_id}/cost-summary?${params}`);
366
+ return toContent(result);
367
+ }
368
+
369
+ async function getStoryTokenUsage({ story_id }) {
370
+ const result = await apiCall("GET", `/api/v1/stories/${story_id}/token-usage`);
371
+ return toContent(result);
372
+ }
373
+
374
+ async function getCostAnomalies({ project_id }) {
375
+ const params = new URLSearchParams();
376
+ if (project_id) params.set("project_id", project_id);
377
+
378
+ const query = params.toString() ? `?${params}` : "";
379
+ const result = await apiCall("GET", `/api/v1/cost-anomalies${query}`);
380
+ return toContent(result);
381
+ }
382
+
383
+ async function setTokenBudget({ scope_type, scope_id, budget_millicents, alert_threshold_pct }) {
384
+ const body = { scope_type, scope_id, budget_millicents };
385
+ if (alert_threshold_pct != null) body.alert_threshold_pct = alert_threshold_pct;
386
+
387
+ const result = await apiCall(
388
+ "POST",
389
+ "/api/v1/token-budgets",
390
+ body,
391
+ process.env.LOOPCTL_ORCH_KEY
392
+ );
393
+ return toContent(result);
394
+ }
395
+
311
396
  // --- Discovery Tools ---
312
397
 
313
398
  async function listRoutes() {
@@ -356,7 +441,7 @@ const TOOLS = [
356
441
  },
357
442
  {
358
443
  name: "get_progress",
359
- description: "Get progress summary for a project, including story counts by status.",
444
+ description: "Get progress summary for a project, including story counts by status. Pass include_cost=true to include cost data when available.",
360
445
  inputSchema: {
361
446
  type: "object",
362
447
  properties: {
@@ -364,6 +449,10 @@ const TOOLS = [
364
449
  type: "string",
365
450
  description: "The UUID of the project.",
366
451
  },
452
+ include_cost: {
453
+ type: "boolean",
454
+ description: "Optional: include cost/token summary data in the response.",
455
+ },
367
456
  },
368
457
  required: ["project_id"],
369
458
  },
@@ -391,7 +480,9 @@ const TOOLS = [
391
480
  {
392
481
  name: "list_stories",
393
482
  description:
394
- "List stories for a project, optionally filtered by agent_status, verified_status, or epic_id.",
483
+ "List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. " +
484
+ "Returns compact results (no acceptance_criteria/description) — use get_story for full details. " +
485
+ "Defaults to 20 stories per page; use limit/offset to paginate.",
395
486
  inputSchema: {
396
487
  type: "object",
397
488
  properties: {
@@ -420,6 +511,10 @@ const TOOLS = [
420
511
  type: "integer",
421
512
  description: "Number of stories to skip (for pagination).",
422
513
  },
514
+ include_token_totals: {
515
+ type: "boolean",
516
+ description: "Optional: include per-story token usage totals when available.",
517
+ },
423
518
  },
424
519
  required: ["project_id"],
425
520
  },
@@ -427,7 +522,8 @@ const TOOLS = [
427
522
  {
428
523
  name: "list_ready_stories",
429
524
  description:
430
- "List stories that are ready to be worked on (contracted, dependencies met). These are the stories an agent should pick up next.",
525
+ "List stories that are ready to be worked on (contracted, dependencies met). " +
526
+ "Returns compact results — use get_story for full details. Defaults to 20 per page.",
431
527
  inputSchema: {
432
528
  type: "object",
433
529
  properties: {
@@ -554,6 +650,16 @@ const TOOLS = [
554
650
  type: "string",
555
651
  description: "Optional: path or URL of the artifact.",
556
652
  },
653
+ token_usage: {
654
+ type: "object",
655
+ description: "Optional: token usage summary for the implementation work.",
656
+ properties: {
657
+ input_tokens: { type: "integer", description: "Total input tokens consumed." },
658
+ output_tokens: { type: "integer", description: "Total output tokens consumed." },
659
+ model_name: { type: "string", description: "Model name (e.g. claude-sonnet-4-5)." },
660
+ cost_millicents: { type: "integer", description: "Total cost in millicents (1/1000 of a cent)." },
661
+ },
662
+ },
557
663
  },
558
664
  required: ["story_id"],
559
665
  },
@@ -703,6 +809,134 @@ const TOOLS = [
703
809
  },
704
810
  },
705
811
 
812
+ // Token Efficiency Tools
813
+ {
814
+ name: "report_token_usage",
815
+ description:
816
+ "Report token usage for a story implementation session. " +
817
+ "Stores input/output token counts, model name, and cost. Uses the AGENT key.",
818
+ inputSchema: {
819
+ type: "object",
820
+ properties: {
821
+ story_id: {
822
+ type: "string",
823
+ description: "The UUID of the story this usage is attributed to.",
824
+ },
825
+ input_tokens: {
826
+ type: "integer",
827
+ description: "Number of input (prompt) tokens consumed.",
828
+ },
829
+ output_tokens: {
830
+ type: "integer",
831
+ description: "Number of output (completion) tokens consumed.",
832
+ },
833
+ model_name: {
834
+ type: "string",
835
+ description: "Name of the model used (e.g. claude-sonnet-4-5, gpt-4o).",
836
+ },
837
+ cost_millicents: {
838
+ type: "integer",
839
+ description: "Total cost in millicents (1/1000 of a cent).",
840
+ },
841
+ phase: {
842
+ type: "string",
843
+ description: "Optional: phase of work (e.g. implement, review, verify).",
844
+ },
845
+ skill_version_id: {
846
+ type: "string",
847
+ description: "Optional: UUID of the skill version used.",
848
+ },
849
+ session_id: {
850
+ type: "string",
851
+ description: "Optional: agent session identifier for grouping records.",
852
+ },
853
+ },
854
+ required: ["story_id", "input_tokens", "output_tokens", "model_name", "cost_millicents"],
855
+ },
856
+ },
857
+ {
858
+ name: "get_cost_summary",
859
+ description:
860
+ "Get cost/token usage summary for a project. " +
861
+ "Optionally break down by agent, epic, or model.",
862
+ inputSchema: {
863
+ type: "object",
864
+ properties: {
865
+ project_id: {
866
+ type: "string",
867
+ description: "The UUID of the project.",
868
+ },
869
+ breakdown: {
870
+ type: "string",
871
+ enum: ["agent", "epic", "model"],
872
+ description: "Optional: dimension to group the summary by (agent, epic, or model).",
873
+ },
874
+ },
875
+ required: ["project_id"],
876
+ },
877
+ },
878
+ {
879
+ name: "get_story_token_usage",
880
+ description: "Get token usage records for a single story.",
881
+ inputSchema: {
882
+ type: "object",
883
+ properties: {
884
+ story_id: {
885
+ type: "string",
886
+ description: "The UUID of the story.",
887
+ },
888
+ },
889
+ required: ["story_id"],
890
+ },
891
+ },
892
+ {
893
+ name: "get_cost_anomalies",
894
+ description:
895
+ "Get cost anomaly alerts — stories or agents that exceed expected token budgets. " +
896
+ "Optionally filter by project.",
897
+ inputSchema: {
898
+ type: "object",
899
+ properties: {
900
+ project_id: {
901
+ type: "string",
902
+ description: "Optional: filter anomalies to a specific project UUID.",
903
+ },
904
+ },
905
+ required: [],
906
+ },
907
+ },
908
+ {
909
+ name: "set_token_budget",
910
+ description:
911
+ "Set a token budget for a scope (project, epic, story, or agent). " +
912
+ "Requires orchestrator or user role. Uses the ORCH key.",
913
+ inputSchema: {
914
+ type: "object",
915
+ properties: {
916
+ scope_type: {
917
+ type: "string",
918
+ enum: ["project", "epic", "story", "agent"],
919
+ description: "The type of scope to apply the budget to.",
920
+ },
921
+ scope_id: {
922
+ type: "string",
923
+ description: "The UUID of the scoped resource (project_id, epic_id, story_id, or agent_id).",
924
+ },
925
+ budget_millicents: {
926
+ type: "integer",
927
+ description: "Maximum allowed cost in millicents (1/1000 of a cent).",
928
+ },
929
+ alert_threshold_pct: {
930
+ type: "number",
931
+ description: "Optional: percentage of budget at which to trigger an alert (0–100).",
932
+ minimum: 0,
933
+ maximum: 100,
934
+ },
935
+ },
936
+ required: ["scope_type", "scope_id", "budget_millicents"],
937
+ },
938
+ },
939
+
706
940
  // Discovery Tools
707
941
  {
708
942
  name: "list_routes",
@@ -722,7 +956,7 @@ const TOOLS = [
722
956
  const server = new Server(
723
957
  {
724
958
  name: "loopctl",
725
- version: "1.0.0",
959
+ version: "1.1.0",
726
960
  },
727
961
  {
728
962
  capabilities: { tools: {} },
@@ -797,6 +1031,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
797
1031
  case "verify_all_in_epic":
798
1032
  return await verifyAllInEpic(args);
799
1033
 
1034
+ // Token Efficiency Tools
1035
+ case "report_token_usage":
1036
+ return await reportTokenUsage(args);
1037
+
1038
+ case "get_cost_summary":
1039
+ return await getCostSummary(args);
1040
+
1041
+ case "get_story_token_usage":
1042
+ return await getStoryTokenUsage(args);
1043
+
1044
+ case "get_cost_anomalies":
1045
+ return await getCostAnomalies(args);
1046
+
1047
+ case "set_token_budget":
1048
+ return await setTokenBudget(args);
1049
+
800
1050
  // Discovery Tools
801
1051
  case "list_routes":
802
1052
  return await listRoutes();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",