loopctl-mcp-server 1.0.2 → 1.1.1

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 +236 -6
  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
@@ -166,8 +166,11 @@ async function createProject({ name, slug, repo_url, description, tech_stack })
166
166
  return toContent(result);
167
167
  }
168
168
 
169
- async function getProgress({ project_id }) {
170
- 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}`);
171
174
  return toContent(result);
172
175
  }
173
176
 
@@ -183,13 +186,14 @@ async function importStories({ project_id, payload }) {
183
186
 
184
187
  // --- Story Tools ---
185
188
 
186
- 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 }) {
187
190
  const params = new URLSearchParams({ project_id });
188
191
  if (agent_status) params.set("agent_status", agent_status);
189
192
  if (verified_status) params.set("verified_status", verified_status);
190
193
  if (epic_id) params.set("epic_id", epic_id);
191
194
  params.set("limit", String(limit ?? 20));
192
195
  if (offset != null) params.set("offset", String(offset));
196
+ if (include_token_totals) params.set("include_token_totals", "true");
193
197
 
194
198
  const result = await apiCall("GET", `/api/v1/stories?${params}`);
195
199
  return toContentCompact(result);
@@ -252,13 +256,16 @@ async function requestReview({ story_id }) {
252
256
 
253
257
  // --- Reviewer Tools (orch key — reviewer uses orchestrator role) ---
254
258
 
255
- async function reportStory({ story_id, artifact_type, artifact_path }) {
259
+ async function reportStory({ story_id, artifact_type, artifact_path, token_usage }) {
256
260
  const body = {};
257
261
  if (artifact_type || artifact_path) {
258
262
  body.artifact = {};
259
263
  if (artifact_type) body.artifact.artifact_type = artifact_type;
260
264
  if (artifact_path) body.artifact.path = artifact_path;
261
265
  }
266
+ if (token_usage) {
267
+ body.token_usage = token_usage;
268
+ }
262
269
 
263
270
  const result = await apiCall(
264
271
  "POST",
@@ -334,6 +341,66 @@ async function verifyAllInEpic({ epic_id, review_type, summary }) {
334
341
  return toContent(result);
335
342
  }
336
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
+ let path;
363
+ if (breakdown === "agent") {
364
+ path = `/api/v1/analytics/agents?project_id=${project_id}`;
365
+ } else if (breakdown === "epic") {
366
+ path = `/api/v1/analytics/epics?project_id=${project_id}`;
367
+ } else if (breakdown === "model") {
368
+ path = `/api/v1/analytics/models?project_id=${project_id}`;
369
+ } else {
370
+ path = `/api/v1/analytics/projects/${project_id}`;
371
+ }
372
+
373
+ const result = await apiCall("GET", path);
374
+ return toContent(result);
375
+ }
376
+
377
+ async function getStoryTokenUsage({ story_id }) {
378
+ const result = await apiCall("GET", `/api/v1/stories/${story_id}/token-usage`);
379
+ return toContent(result);
380
+ }
381
+
382
+ async function getCostAnomalies({ project_id }) {
383
+ const params = new URLSearchParams();
384
+ if (project_id) params.set("project_id", project_id);
385
+
386
+ const query = params.toString() ? `?${params}` : "";
387
+ const result = await apiCall("GET", `/api/v1/cost-anomalies${query}`);
388
+ return toContent(result);
389
+ }
390
+
391
+ async function setTokenBudget({ scope_type, scope_id, budget_millicents, alert_threshold_pct }) {
392
+ const body = { scope_type, scope_id, budget_millicents };
393
+ if (alert_threshold_pct != null) body.alert_threshold_pct = alert_threshold_pct;
394
+
395
+ const result = await apiCall(
396
+ "POST",
397
+ "/api/v1/token-budgets",
398
+ body,
399
+ process.env.LOOPCTL_ORCH_KEY
400
+ );
401
+ return toContent(result);
402
+ }
403
+
337
404
  // --- Discovery Tools ---
338
405
 
339
406
  async function listRoutes() {
@@ -382,7 +449,7 @@ const TOOLS = [
382
449
  },
383
450
  {
384
451
  name: "get_progress",
385
- description: "Get progress summary for a project, including story counts by status.",
452
+ description: "Get progress summary for a project, including story counts by status. Pass include_cost=true to include cost data when available.",
386
453
  inputSchema: {
387
454
  type: "object",
388
455
  properties: {
@@ -390,6 +457,10 @@ const TOOLS = [
390
457
  type: "string",
391
458
  description: "The UUID of the project.",
392
459
  },
460
+ include_cost: {
461
+ type: "boolean",
462
+ description: "Optional: include cost/token summary data in the response.",
463
+ },
393
464
  },
394
465
  required: ["project_id"],
395
466
  },
@@ -448,6 +519,10 @@ const TOOLS = [
448
519
  type: "integer",
449
520
  description: "Number of stories to skip (for pagination).",
450
521
  },
522
+ include_token_totals: {
523
+ type: "boolean",
524
+ description: "Optional: include per-story token usage totals when available.",
525
+ },
451
526
  },
452
527
  required: ["project_id"],
453
528
  },
@@ -583,6 +658,16 @@ const TOOLS = [
583
658
  type: "string",
584
659
  description: "Optional: path or URL of the artifact.",
585
660
  },
661
+ token_usage: {
662
+ type: "object",
663
+ description: "Optional: token usage summary for the implementation work.",
664
+ properties: {
665
+ input_tokens: { type: "integer", description: "Total input tokens consumed." },
666
+ output_tokens: { type: "integer", description: "Total output tokens consumed." },
667
+ model_name: { type: "string", description: "Model name (e.g. claude-sonnet-4-5)." },
668
+ cost_millicents: { type: "integer", description: "Total cost in millicents (1/1000 of a cent)." },
669
+ },
670
+ },
586
671
  },
587
672
  required: ["story_id"],
588
673
  },
@@ -732,6 +817,135 @@ const TOOLS = [
732
817
  },
733
818
  },
734
819
 
820
+ // Token Efficiency Tools
821
+ {
822
+ name: "report_token_usage",
823
+ description:
824
+ "Report token usage for a story implementation session. " +
825
+ "Stores input/output token counts, model name, and cost. Uses the AGENT key.",
826
+ inputSchema: {
827
+ type: "object",
828
+ properties: {
829
+ story_id: {
830
+ type: "string",
831
+ description: "The UUID of the story this usage is attributed to.",
832
+ },
833
+ input_tokens: {
834
+ type: "integer",
835
+ description: "Number of input (prompt) tokens consumed.",
836
+ },
837
+ output_tokens: {
838
+ type: "integer",
839
+ description: "Number of output (completion) tokens consumed.",
840
+ },
841
+ model_name: {
842
+ type: "string",
843
+ description: "Name of the model used (e.g. claude-sonnet-4-5, gpt-4o).",
844
+ },
845
+ cost_millicents: {
846
+ type: "integer",
847
+ description: "Total cost in millicents (1/1000 of a cent).",
848
+ },
849
+ phase: {
850
+ type: "string",
851
+ enum: ["planning", "implementing", "reviewing", "other"],
852
+ description: "Optional: phase of work.",
853
+ },
854
+ skill_version_id: {
855
+ type: "string",
856
+ description: "Optional: UUID of the skill version used.",
857
+ },
858
+ session_id: {
859
+ type: "string",
860
+ description: "Optional: agent session identifier for grouping records.",
861
+ },
862
+ },
863
+ required: ["story_id", "input_tokens", "output_tokens", "model_name", "cost_millicents"],
864
+ },
865
+ },
866
+ {
867
+ name: "get_cost_summary",
868
+ description:
869
+ "Get cost/token usage summary for a project. " +
870
+ "Optionally break down by agent, epic, or model.",
871
+ inputSchema: {
872
+ type: "object",
873
+ properties: {
874
+ project_id: {
875
+ type: "string",
876
+ description: "The UUID of the project.",
877
+ },
878
+ breakdown: {
879
+ type: "string",
880
+ enum: ["agent", "epic", "model"],
881
+ description: "Optional: dimension to group the summary by (agent, epic, or model).",
882
+ },
883
+ },
884
+ required: ["project_id"],
885
+ },
886
+ },
887
+ {
888
+ name: "get_story_token_usage",
889
+ description: "Get token usage records for a single story.",
890
+ inputSchema: {
891
+ type: "object",
892
+ properties: {
893
+ story_id: {
894
+ type: "string",
895
+ description: "The UUID of the story.",
896
+ },
897
+ },
898
+ required: ["story_id"],
899
+ },
900
+ },
901
+ {
902
+ name: "get_cost_anomalies",
903
+ description:
904
+ "Get cost anomaly alerts — stories or agents that exceed expected token budgets. " +
905
+ "Optionally filter by project.",
906
+ inputSchema: {
907
+ type: "object",
908
+ properties: {
909
+ project_id: {
910
+ type: "string",
911
+ description: "Optional: filter anomalies to a specific project UUID.",
912
+ },
913
+ },
914
+ required: [],
915
+ },
916
+ },
917
+ {
918
+ name: "set_token_budget",
919
+ description:
920
+ "Set a token budget for a scope (project, epic, story, or agent). " +
921
+ "Requires orchestrator or user role. Uses the ORCH key.",
922
+ inputSchema: {
923
+ type: "object",
924
+ properties: {
925
+ scope_type: {
926
+ type: "string",
927
+ enum: ["project", "epic", "story", "agent"],
928
+ description: "The type of scope to apply the budget to.",
929
+ },
930
+ scope_id: {
931
+ type: "string",
932
+ description: "The UUID of the scoped resource (project_id, epic_id, story_id, or agent_id).",
933
+ },
934
+ budget_millicents: {
935
+ type: "integer",
936
+ description: "Maximum allowed cost in millicents (1/1000 of a cent).",
937
+ },
938
+ alert_threshold_pct: {
939
+ type: "number",
940
+ description: "Optional: percentage of budget at which to trigger an alert (0–100).",
941
+ minimum: 0,
942
+ maximum: 100,
943
+ },
944
+ },
945
+ required: ["scope_type", "scope_id", "budget_millicents"],
946
+ },
947
+ },
948
+
735
949
  // Discovery Tools
736
950
  {
737
951
  name: "list_routes",
@@ -751,7 +965,7 @@ const TOOLS = [
751
965
  const server = new Server(
752
966
  {
753
967
  name: "loopctl",
754
- version: "1.0.0",
968
+ version: "1.1.1",
755
969
  },
756
970
  {
757
971
  capabilities: { tools: {} },
@@ -826,6 +1040,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
826
1040
  case "verify_all_in_epic":
827
1041
  return await verifyAllInEpic(args);
828
1042
 
1043
+ // Token Efficiency Tools
1044
+ case "report_token_usage":
1045
+ return await reportTokenUsage(args);
1046
+
1047
+ case "get_cost_summary":
1048
+ return await getCostSummary(args);
1049
+
1050
+ case "get_story_token_usage":
1051
+ return await getStoryTokenUsage(args);
1052
+
1053
+ case "get_cost_anomalies":
1054
+ return await getCostAnomalies(args);
1055
+
1056
+ case "set_token_budget":
1057
+ return await setTokenBudget(args);
1058
+
829
1059
  // Discovery Tools
830
1060
  case "list_routes":
831
1061
  return await listRoutes();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",