loopctl-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/index.js +817 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Michael Kreyman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# loopctl-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for [loopctl](https://loopctl.com) -- structural trust for AI development loops.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install loopctl-mcp-server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run directly with npx:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx loopctl-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Add to your `.mcp.json` (Claude Code) or equivalent MCP config:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"loopctl": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["loopctl-mcp-server"],
|
|
29
|
+
"env": {
|
|
30
|
+
"LOOPCTL_SERVER": "https://loopctl.com",
|
|
31
|
+
"LOOPCTL_ORCH_KEY": "lc_your_orchestrator_key",
|
|
32
|
+
"LOOPCTL_AGENT_KEY": "lc_your_agent_key"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or if installed locally:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"loopctl": {
|
|
45
|
+
"command": "node",
|
|
46
|
+
"args": ["node_modules/loopctl-mcp-server/index.js"],
|
|
47
|
+
"env": {
|
|
48
|
+
"LOOPCTL_SERVER": "https://loopctl.com",
|
|
49
|
+
"LOOPCTL_ORCH_KEY": "lc_your_orchestrator_key",
|
|
50
|
+
"LOOPCTL_AGENT_KEY": "lc_your_agent_key"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Environment Variables
|
|
58
|
+
|
|
59
|
+
| Variable | Description | Default |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `LOOPCTL_SERVER` | loopctl server URL | `https://loopctl.com` |
|
|
62
|
+
| `LOOPCTL_API_KEY` | Global API key override (if set, always used) | -- |
|
|
63
|
+
| `LOOPCTL_ORCH_KEY` | Orchestrator role API key (verify, reject, review, import) | -- |
|
|
64
|
+
| `LOOPCTL_AGENT_KEY` | Agent role API key (contract, claim, start, request-review) | -- |
|
|
65
|
+
|
|
66
|
+
Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
|
|
67
|
+
|
|
68
|
+
## Tools (19)
|
|
69
|
+
|
|
70
|
+
### Project Tools
|
|
71
|
+
|
|
72
|
+
| Tool | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `get_tenant` | Get current tenant info. Use to verify connectivity. |
|
|
75
|
+
| `list_projects` | List all projects in the current tenant. |
|
|
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. |
|
|
78
|
+
| `import_stories` | Import stories into a project from a structured payload (Epic 12 import format). |
|
|
79
|
+
|
|
80
|
+
### Story Tools
|
|
81
|
+
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `list_stories` | List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. |
|
|
85
|
+
| `list_ready_stories` | List stories that are ready to be worked on (contracted, dependencies met). |
|
|
86
|
+
| `get_story` | Get full details for a single story by ID. |
|
|
87
|
+
|
|
88
|
+
### Workflow Tools (agent key)
|
|
89
|
+
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `contract_story` | Agent acknowledges a story's acceptance criteria. Transitions pending -> contracted. |
|
|
93
|
+
| `claim_story` | Agent claims a contracted story with pessimistic locking. Transitions contracted -> assigned. |
|
|
94
|
+
| `start_story` | Agent starts work on a claimed story. Transitions assigned -> implementing. |
|
|
95
|
+
| `request_review` | Agent signals implementation is complete and ready for review. |
|
|
96
|
+
|
|
97
|
+
### Reviewer Tools (orchestrator key)
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. |
|
|
102
|
+
| `review_complete` | Record that a review has been completed for a story. Required before verify. |
|
|
103
|
+
|
|
104
|
+
### Verification Tools (orchestrator key)
|
|
105
|
+
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. |
|
|
109
|
+
| `reject_story` | Orchestrator rejects a story with a reason. |
|
|
110
|
+
|
|
111
|
+
### Bulk Tools (orchestrator key)
|
|
112
|
+
|
|
113
|
+
| Tool | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `bulk_mark_complete` | Bulk mark multiple stories as complete in a single API call. |
|
|
116
|
+
| `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. |
|
|
117
|
+
|
|
118
|
+
### Discovery Tools
|
|
119
|
+
|
|
120
|
+
| Tool | Description |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `list_routes` | List all available API routes on the loopctl server. |
|
|
123
|
+
|
|
124
|
+
## Chain-of-Custody Enforcement
|
|
125
|
+
|
|
126
|
+
loopctl enforces that nobody marks their own work as done. The API returns `409` if the caller's identity matches the story's assigned agent:
|
|
127
|
+
|
|
128
|
+
- `report_story` -- 409 `self_report_blocked`
|
|
129
|
+
- `review_complete` -- 409 `self_review_blocked`
|
|
130
|
+
- `verify_story` -- 409 `self_verify_blocked`
|
|
131
|
+
|
|
132
|
+
The implementer's final action is `request_review`. All subsequent steps (report, review, verify) must come from different agents.
|
|
133
|
+
|
|
134
|
+
## Troubleshooting
|
|
135
|
+
|
|
136
|
+
### Connection errors
|
|
137
|
+
|
|
138
|
+
- Verify `LOOPCTL_SERVER` is set and reachable
|
|
139
|
+
- Check that the server URL includes the protocol (`https://`)
|
|
140
|
+
- If using a self-signed certificate, set `NODE_TLS_REJECT_UNAUTHORIZED=0` in your environment (not recommended for production)
|
|
141
|
+
|
|
142
|
+
### Authentication errors (401)
|
|
143
|
+
|
|
144
|
+
- Verify your API key is correct and active
|
|
145
|
+
- Check that the key has the right role for the operation (agent vs orchestrator)
|
|
146
|
+
- Keys are prefixed with `lc_` -- ensure the full key is provided
|
|
147
|
+
|
|
148
|
+
### Permission errors (403)
|
|
149
|
+
|
|
150
|
+
- Orchestrator operations require an orchestrator-role key
|
|
151
|
+
- Agent operations require an agent-role key
|
|
152
|
+
- Chain-of-custody violations return 409, not 403
|
|
153
|
+
|
|
154
|
+
### Tool not found
|
|
155
|
+
|
|
156
|
+
- Ensure the MCP server is running (`npx loopctl-mcp-server` to test)
|
|
157
|
+
- Check your `.mcp.json` configuration syntax
|
|
158
|
+
- Restart your AI coding tool after configuration changes
|
|
159
|
+
|
|
160
|
+
## Links
|
|
161
|
+
|
|
162
|
+
- [loopctl.com](https://loopctl.com) -- landing page and documentation
|
|
163
|
+
- [API docs](https://loopctl.com/swaggerui) -- Swagger UI
|
|
164
|
+
- [GitHub](https://github.com/mkreyman/loopctl) -- source code
|
|
165
|
+
- [npm](https://www.npmjs.com/package/loopctl-mcp-server) -- npm package
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// loopctl MCP Server
|
|
4
|
+
// Wraps the loopctl REST API into typed MCP tools for Claude Code agents.
|
|
5
|
+
// Runs via stdio (stdin/stdout).
|
|
6
|
+
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import {
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
CallToolRequestSchema,
|
|
12
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// HTTP helper
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function getBaseUrl() {
|
|
19
|
+
return (process.env.LOOPCTL_SERVER || "https://loopctl.com").replace(/\/$/, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve which API key to use for a request.
|
|
24
|
+
*
|
|
25
|
+
* Priority:
|
|
26
|
+
* 1. LOOPCTL_API_KEY (global override — if set, always used)
|
|
27
|
+
* 2. keyOverride passed by the tool function (role-specific key)
|
|
28
|
+
* 3. LOOPCTL_ORCH_KEY (safe default for reads)
|
|
29
|
+
*/
|
|
30
|
+
function resolveKey(keyOverride) {
|
|
31
|
+
return (
|
|
32
|
+
process.env.LOOPCTL_API_KEY ||
|
|
33
|
+
keyOverride ||
|
|
34
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function apiCall(method, path, body, keyOverride) {
|
|
39
|
+
const url = `${getBaseUrl()}${path}`;
|
|
40
|
+
const key = resolveKey(keyOverride);
|
|
41
|
+
|
|
42
|
+
if (!key) {
|
|
43
|
+
return { error: true, status: 0, body: "No API key configured. Set LOOPCTL_API_KEY, LOOPCTL_ORCH_KEY, or LOOPCTL_AGENT_KEY." };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const options = {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${key}`,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
Accept: "application/json",
|
|
52
|
+
},
|
|
53
|
+
signal: AbortSignal.timeout(30_000),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (body !== undefined && body !== null) {
|
|
57
|
+
options.body = JSON.stringify(body);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await fetch(url, options);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err.name === "TimeoutError") {
|
|
65
|
+
return { error: true, status: 0, body: "Request timed out after 30s" };
|
|
66
|
+
}
|
|
67
|
+
const cause = err.cause?.message ? ` (${err.cause.message})` : "";
|
|
68
|
+
return { error: true, status: 0, body: `Network error: ${err.message}${cause}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (response.status === 204) {
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let responseBody;
|
|
76
|
+
const contentType = response.headers.get("content-type") || "";
|
|
77
|
+
if (contentType.includes("application/json")) {
|
|
78
|
+
responseBody = await response.json();
|
|
79
|
+
} else {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
try {
|
|
82
|
+
responseBody = JSON.parse(text);
|
|
83
|
+
} catch {
|
|
84
|
+
responseBody = text;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
let errorBody = responseBody;
|
|
90
|
+
if (typeof errorBody === "string" && errorBody.length > 500) {
|
|
91
|
+
errorBody = errorBody
|
|
92
|
+
.replace(/<[^>]+>/g, " ")
|
|
93
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
94
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ")
|
|
95
|
+
.replace(/\s+/g, " ")
|
|
96
|
+
.trim()
|
|
97
|
+
.slice(0, 500) + "... (truncated)";
|
|
98
|
+
}
|
|
99
|
+
return { error: true, status: response.status, body: errorBody };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return responseBody;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toContent(result) {
|
|
106
|
+
const isErr = result && result.error === true;
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: JSON.stringify(result, null, 2),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
...(isErr && { isError: true }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Tool implementations
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
// --- Project Tools ---
|
|
123
|
+
|
|
124
|
+
async function getTenant() {
|
|
125
|
+
const result = await apiCall("GET", "/api/v1/tenants/me");
|
|
126
|
+
return toContent(result);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function listProjects() {
|
|
130
|
+
const result = await apiCall("GET", "/api/v1/projects");
|
|
131
|
+
return toContent(result);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function createProject({ name, slug, repo_url, description, tech_stack }) {
|
|
135
|
+
const body = { name, slug };
|
|
136
|
+
if (repo_url) body.repo_url = repo_url;
|
|
137
|
+
if (description) body.description = description;
|
|
138
|
+
if (tech_stack) body.tech_stack = tech_stack;
|
|
139
|
+
const result = await apiCall("POST", "/api/v1/projects", body, process.env.LOOPCTL_ORCH_KEY);
|
|
140
|
+
return toContent(result);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function getProgress({ project_id }) {
|
|
144
|
+
const result = await apiCall("GET", `/api/v1/projects/${project_id}/progress`);
|
|
145
|
+
return toContent(result);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function importStories({ project_id, payload }) {
|
|
149
|
+
const result = await apiCall(
|
|
150
|
+
"POST",
|
|
151
|
+
`/api/v1/projects/${project_id}/import`,
|
|
152
|
+
payload,
|
|
153
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
154
|
+
);
|
|
155
|
+
return toContent(result);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Story Tools ---
|
|
159
|
+
|
|
160
|
+
async function listStories({ project_id, agent_status, verified_status, epic_id, limit, offset }) {
|
|
161
|
+
const params = new URLSearchParams({ project_id });
|
|
162
|
+
if (agent_status) params.set("agent_status", agent_status);
|
|
163
|
+
if (verified_status) params.set("verified_status", verified_status);
|
|
164
|
+
if (epic_id) params.set("epic_id", epic_id);
|
|
165
|
+
if (limit != null) params.set("limit", String(limit));
|
|
166
|
+
if (offset != null) params.set("offset", String(offset));
|
|
167
|
+
|
|
168
|
+
const result = await apiCall("GET", `/api/v1/stories?${params}`);
|
|
169
|
+
return toContent(result);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function listReadyStories({ project_id, limit }) {
|
|
173
|
+
const params = new URLSearchParams({ project_id });
|
|
174
|
+
if (limit != null) params.set("limit", String(limit));
|
|
175
|
+
|
|
176
|
+
const result = await apiCall("GET", `/api/v1/stories/ready?${params}`);
|
|
177
|
+
return toContent(result);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function getStory({ story_id }) {
|
|
181
|
+
const result = await apiCall("GET", `/api/v1/stories/${story_id}`);
|
|
182
|
+
return toContent(result);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Workflow Tools (agent key) ---
|
|
186
|
+
|
|
187
|
+
async function contractStory({ story_id, story_title, ac_count }) {
|
|
188
|
+
const result = await apiCall(
|
|
189
|
+
"POST",
|
|
190
|
+
`/api/v1/stories/${story_id}/contract`,
|
|
191
|
+
{ story_title, ac_count },
|
|
192
|
+
process.env.LOOPCTL_AGENT_KEY
|
|
193
|
+
);
|
|
194
|
+
return toContent(result);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function claimStory({ story_id }) {
|
|
198
|
+
const result = await apiCall(
|
|
199
|
+
"POST",
|
|
200
|
+
`/api/v1/stories/${story_id}/claim`,
|
|
201
|
+
null,
|
|
202
|
+
process.env.LOOPCTL_AGENT_KEY
|
|
203
|
+
);
|
|
204
|
+
return toContent(result);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function startStory({ story_id }) {
|
|
208
|
+
const result = await apiCall(
|
|
209
|
+
"POST",
|
|
210
|
+
`/api/v1/stories/${story_id}/start`,
|
|
211
|
+
null,
|
|
212
|
+
process.env.LOOPCTL_AGENT_KEY
|
|
213
|
+
);
|
|
214
|
+
return toContent(result);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function requestReview({ story_id }) {
|
|
218
|
+
const result = await apiCall(
|
|
219
|
+
"POST",
|
|
220
|
+
`/api/v1/stories/${story_id}/request-review`,
|
|
221
|
+
null,
|
|
222
|
+
process.env.LOOPCTL_AGENT_KEY
|
|
223
|
+
);
|
|
224
|
+
return toContent(result);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Reviewer Tools (orch key — reviewer uses orchestrator role) ---
|
|
228
|
+
|
|
229
|
+
async function reportStory({ story_id, artifact_type, artifact_path }) {
|
|
230
|
+
const body = {};
|
|
231
|
+
if (artifact_type || artifact_path) {
|
|
232
|
+
body.artifact = {};
|
|
233
|
+
if (artifact_type) body.artifact.artifact_type = artifact_type;
|
|
234
|
+
if (artifact_path) body.artifact.path = artifact_path;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result = await apiCall(
|
|
238
|
+
"POST",
|
|
239
|
+
`/api/v1/stories/${story_id}/report`,
|
|
240
|
+
Object.keys(body).length > 0 ? body : null,
|
|
241
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
242
|
+
);
|
|
243
|
+
return toContent(result);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary }) {
|
|
247
|
+
const body = { review_type };
|
|
248
|
+
if (findings_count != null) body.findings_count = findings_count;
|
|
249
|
+
if (fixes_count != null) body.fixes_count = fixes_count;
|
|
250
|
+
if (disproved_count != null) body.disproved_count = disproved_count;
|
|
251
|
+
if (summary) body.summary = summary;
|
|
252
|
+
|
|
253
|
+
const result = await apiCall(
|
|
254
|
+
"POST",
|
|
255
|
+
`/api/v1/stories/${story_id}/review-complete`,
|
|
256
|
+
body,
|
|
257
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
258
|
+
);
|
|
259
|
+
return toContent(result);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Verification Tools (orch key) ---
|
|
263
|
+
|
|
264
|
+
async function verifyStory({ story_id, summary, review_type }) {
|
|
265
|
+
const body = {};
|
|
266
|
+
if (summary) body.summary = summary;
|
|
267
|
+
if (review_type) body.review_type = review_type;
|
|
268
|
+
|
|
269
|
+
const result = await apiCall(
|
|
270
|
+
"POST",
|
|
271
|
+
`/api/v1/stories/${story_id}/verify`,
|
|
272
|
+
body,
|
|
273
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
274
|
+
);
|
|
275
|
+
return toContent(result);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function rejectStory({ story_id, reason }) {
|
|
279
|
+
const result = await apiCall(
|
|
280
|
+
"POST",
|
|
281
|
+
`/api/v1/stories/${story_id}/reject`,
|
|
282
|
+
{ reason },
|
|
283
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
284
|
+
);
|
|
285
|
+
return toContent(result);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- Bulk Tools ---
|
|
289
|
+
|
|
290
|
+
async function bulkMarkComplete({ stories }) {
|
|
291
|
+
// stories: [{story_id, summary, review_type}]
|
|
292
|
+
const result = await apiCall(
|
|
293
|
+
"POST",
|
|
294
|
+
"/api/v1/stories/bulk/mark-complete",
|
|
295
|
+
{ stories },
|
|
296
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
297
|
+
);
|
|
298
|
+
return toContent(result);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function verifyAllInEpic({ epic_id, review_type, summary }) {
|
|
302
|
+
const result = await apiCall(
|
|
303
|
+
"POST",
|
|
304
|
+
`/api/v1/epics/${epic_id}/verify-all`,
|
|
305
|
+
{ review_type, summary },
|
|
306
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
307
|
+
);
|
|
308
|
+
return toContent(result);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Discovery Tools ---
|
|
312
|
+
|
|
313
|
+
async function listRoutes() {
|
|
314
|
+
const result = await apiCall("GET", "/api/v1/routes");
|
|
315
|
+
return toContent(result);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Tool definitions
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
const TOOLS = [
|
|
323
|
+
// Project Tools
|
|
324
|
+
{
|
|
325
|
+
name: "get_tenant",
|
|
326
|
+
description: "Get current tenant info. Use to verify connectivity.",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {},
|
|
330
|
+
required: [],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "list_projects",
|
|
335
|
+
description: "List all projects in the current tenant.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {},
|
|
339
|
+
required: [],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "create_project",
|
|
344
|
+
description: "Create a new project in the current tenant.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
name: { type: "string", description: "Project name." },
|
|
349
|
+
slug: { type: "string", description: "URL-safe slug." },
|
|
350
|
+
repo_url: { type: "string", description: "GitHub repo URL." },
|
|
351
|
+
description: { type: "string", description: "Project description." },
|
|
352
|
+
tech_stack: { type: "string", description: "Tech stack summary." },
|
|
353
|
+
},
|
|
354
|
+
required: ["name", "slug"],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "get_progress",
|
|
359
|
+
description: "Get progress summary for a project, including story counts by status.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
project_id: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "The UUID of the project.",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
required: ["project_id"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "import_stories",
|
|
373
|
+
description: "Import stories into a project from a structured payload (Epic 12 import format).",
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
project_id: {
|
|
378
|
+
type: "string",
|
|
379
|
+
description: "The UUID of the project to import into.",
|
|
380
|
+
},
|
|
381
|
+
payload: {
|
|
382
|
+
type: "object",
|
|
383
|
+
description: "The import payload object (epics + stories structure).",
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
required: ["project_id", "payload"],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
// Story Tools
|
|
391
|
+
{
|
|
392
|
+
name: "list_stories",
|
|
393
|
+
description:
|
|
394
|
+
"List stories for a project, optionally filtered by agent_status, verified_status, or epic_id.",
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: "object",
|
|
397
|
+
properties: {
|
|
398
|
+
project_id: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "The UUID of the project.",
|
|
401
|
+
},
|
|
402
|
+
agent_status: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description:
|
|
405
|
+
"Filter by agent status (e.g. pending, contracted, assigned, implementing, reported_done, verified, rejected).",
|
|
406
|
+
},
|
|
407
|
+
verified_status: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "Filter by verified status (e.g. unverified, verified, rejected).",
|
|
410
|
+
},
|
|
411
|
+
epic_id: {
|
|
412
|
+
type: "string",
|
|
413
|
+
description: "Filter by epic UUID.",
|
|
414
|
+
},
|
|
415
|
+
limit: {
|
|
416
|
+
type: "integer",
|
|
417
|
+
description: "Maximum number of stories to return.",
|
|
418
|
+
},
|
|
419
|
+
offset: {
|
|
420
|
+
type: "integer",
|
|
421
|
+
description: "Number of stories to skip (for pagination).",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
required: ["project_id"],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "list_ready_stories",
|
|
429
|
+
description:
|
|
430
|
+
"List stories that are ready to be worked on (contracted, dependencies met). These are the stories an agent should pick up next.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: "object",
|
|
433
|
+
properties: {
|
|
434
|
+
project_id: {
|
|
435
|
+
type: "string",
|
|
436
|
+
description: "The UUID of the project.",
|
|
437
|
+
},
|
|
438
|
+
limit: {
|
|
439
|
+
type: "integer",
|
|
440
|
+
description: "Maximum number of stories to return.",
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
required: ["project_id"],
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "get_story",
|
|
448
|
+
description: "Get full details for a single story by ID.",
|
|
449
|
+
inputSchema: {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: {
|
|
452
|
+
story_id: {
|
|
453
|
+
type: "string",
|
|
454
|
+
description: "The UUID of the story.",
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
required: ["story_id"],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// Workflow Tools (agent)
|
|
462
|
+
{
|
|
463
|
+
name: "contract_story",
|
|
464
|
+
description:
|
|
465
|
+
"Agent acknowledges a story's acceptance criteria to claim the contract. " +
|
|
466
|
+
"Transitions the story from pending to contracted. " +
|
|
467
|
+
"story_title and ac_count must match the actual story to prevent silent misclaims.",
|
|
468
|
+
inputSchema: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
story_id: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "The UUID of the story.",
|
|
474
|
+
},
|
|
475
|
+
story_title: {
|
|
476
|
+
type: "string",
|
|
477
|
+
description: "Must match the story's title exactly (anti-confusion check).",
|
|
478
|
+
},
|
|
479
|
+
ac_count: {
|
|
480
|
+
type: "integer",
|
|
481
|
+
description: "Must match the number of acceptance criteria in the story.",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
required: ["story_id", "story_title", "ac_count"],
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "claim_story",
|
|
489
|
+
description:
|
|
490
|
+
"Agent claims a contracted story. Uses pessimistic locking to prevent double-claims. " +
|
|
491
|
+
"Transitions contracted -> assigned. Uses the AGENT key.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
story_id: {
|
|
496
|
+
type: "string",
|
|
497
|
+
description: "The UUID of the story.",
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
required: ["story_id"],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "start_story",
|
|
505
|
+
description:
|
|
506
|
+
"Agent starts work on a claimed story. Transitions assigned -> implementing. Uses the AGENT key.",
|
|
507
|
+
inputSchema: {
|
|
508
|
+
type: "object",
|
|
509
|
+
properties: {
|
|
510
|
+
story_id: {
|
|
511
|
+
type: "string",
|
|
512
|
+
description: "The UUID of the story.",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
required: ["story_id"],
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: "request_review",
|
|
520
|
+
description:
|
|
521
|
+
"Agent signals that implementation is complete and ready for review. " +
|
|
522
|
+
"Does NOT change the story status — fires a webhook event for the reviewer. Uses the AGENT key.",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: "object",
|
|
525
|
+
properties: {
|
|
526
|
+
story_id: {
|
|
527
|
+
type: "string",
|
|
528
|
+
description: "The UUID of the story.",
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
required: ["story_id"],
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
// Reviewer Tools (orch key)
|
|
536
|
+
{
|
|
537
|
+
name: "report_story",
|
|
538
|
+
description:
|
|
539
|
+
"Reviewer (a DIFFERENT agent from the implementer) confirms the implementation is done. " +
|
|
540
|
+
"Chain-of-custody: the implementing agent cannot call this. " +
|
|
541
|
+
"Transitions implementing -> reported_done. Uses the ORCH key.",
|
|
542
|
+
inputSchema: {
|
|
543
|
+
type: "object",
|
|
544
|
+
properties: {
|
|
545
|
+
story_id: {
|
|
546
|
+
type: "string",
|
|
547
|
+
description: "The UUID of the story.",
|
|
548
|
+
},
|
|
549
|
+
artifact_type: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Optional: type of artifact being reported (e.g. branch, pr, test_run).",
|
|
552
|
+
},
|
|
553
|
+
artifact_path: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Optional: path or URL of the artifact.",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
required: ["story_id"],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "review_complete",
|
|
563
|
+
description:
|
|
564
|
+
"Record that a review has been completed for a story. " +
|
|
565
|
+
"Must be called before verify_story. Uses the ORCH key.",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: "object",
|
|
568
|
+
properties: {
|
|
569
|
+
story_id: {
|
|
570
|
+
type: "string",
|
|
571
|
+
description: "The UUID of the story.",
|
|
572
|
+
},
|
|
573
|
+
review_type: {
|
|
574
|
+
type: "string",
|
|
575
|
+
description:
|
|
576
|
+
"The type of review conducted (e.g. enhanced_6_agent, single_reviewer, orchestrator).",
|
|
577
|
+
},
|
|
578
|
+
findings_count: {
|
|
579
|
+
type: "integer",
|
|
580
|
+
description: "Optional: number of findings from the review.",
|
|
581
|
+
},
|
|
582
|
+
fixes_count: {
|
|
583
|
+
type: "integer",
|
|
584
|
+
description: "Number of fixes applied. fixes_count + disproved_count must equal findings_count.",
|
|
585
|
+
},
|
|
586
|
+
disproved_count: {
|
|
587
|
+
type: "integer",
|
|
588
|
+
description: "Number of findings disproved as false positives. fixes_count + disproved_count must equal findings_count.",
|
|
589
|
+
},
|
|
590
|
+
summary: {
|
|
591
|
+
type: "string",
|
|
592
|
+
description: "Optional: summary of the review outcome.",
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
required: ["story_id", "review_type"],
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
// Verification Tools (orch key)
|
|
600
|
+
{
|
|
601
|
+
name: "verify_story",
|
|
602
|
+
description:
|
|
603
|
+
"Orchestrator verifies a reported_done story. " +
|
|
604
|
+
"Requires a review_record to exist (call review_complete first). " +
|
|
605
|
+
"Transitions reported_done -> verified. Uses the ORCH key.",
|
|
606
|
+
inputSchema: {
|
|
607
|
+
type: "object",
|
|
608
|
+
properties: {
|
|
609
|
+
story_id: {
|
|
610
|
+
type: "string",
|
|
611
|
+
description: "The UUID of the story.",
|
|
612
|
+
},
|
|
613
|
+
summary: {
|
|
614
|
+
type: "string",
|
|
615
|
+
description: "Optional: verification summary.",
|
|
616
|
+
},
|
|
617
|
+
review_type: {
|
|
618
|
+
type: "string",
|
|
619
|
+
description: "Optional: review type for the verification record.",
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
required: ["story_id"],
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "reject_story",
|
|
627
|
+
description:
|
|
628
|
+
"Orchestrator rejects a story with a reason. " +
|
|
629
|
+
"Creates a verification_result with result=fail. Uses the ORCH key.",
|
|
630
|
+
inputSchema: {
|
|
631
|
+
type: "object",
|
|
632
|
+
properties: {
|
|
633
|
+
story_id: {
|
|
634
|
+
type: "string",
|
|
635
|
+
description: "The UUID of the story.",
|
|
636
|
+
},
|
|
637
|
+
reason: {
|
|
638
|
+
type: "string",
|
|
639
|
+
description: "Required: the reason for rejection.",
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
required: ["story_id", "reason"],
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
// Bulk Tools
|
|
647
|
+
{
|
|
648
|
+
name: "bulk_mark_complete",
|
|
649
|
+
description:
|
|
650
|
+
"Bulk mark multiple stories as complete in a single API call. " +
|
|
651
|
+
"Each story entry needs a story_id, summary, and review_type. Uses the ORCH key.",
|
|
652
|
+
inputSchema: {
|
|
653
|
+
type: "object",
|
|
654
|
+
properties: {
|
|
655
|
+
stories: {
|
|
656
|
+
type: "array",
|
|
657
|
+
description: "Array of stories to mark complete.",
|
|
658
|
+
items: {
|
|
659
|
+
type: "object",
|
|
660
|
+
properties: {
|
|
661
|
+
story_id: {
|
|
662
|
+
type: "string",
|
|
663
|
+
description: "The UUID of the story.",
|
|
664
|
+
},
|
|
665
|
+
summary: {
|
|
666
|
+
type: "string",
|
|
667
|
+
description: "Summary of the completion.",
|
|
668
|
+
},
|
|
669
|
+
review_type: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "Review type used.",
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
required: ["story_id", "summary", "review_type"],
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
required: ["stories"],
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: "verify_all_in_epic",
|
|
683
|
+
description:
|
|
684
|
+
"Bulk verify all reported_done, unverified stories in an epic. " +
|
|
685
|
+
"Convenience endpoint for the orchestrator after a review pass. Uses the ORCH key.",
|
|
686
|
+
inputSchema: {
|
|
687
|
+
type: "object",
|
|
688
|
+
properties: {
|
|
689
|
+
epic_id: {
|
|
690
|
+
type: "string",
|
|
691
|
+
description: "The UUID of the epic.",
|
|
692
|
+
},
|
|
693
|
+
review_type: {
|
|
694
|
+
type: "string",
|
|
695
|
+
description: "The review type applied to all stories (e.g. enhanced_6_agent).",
|
|
696
|
+
},
|
|
697
|
+
summary: {
|
|
698
|
+
type: "string",
|
|
699
|
+
description: "Summary of the review/verification pass.",
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
required: ["epic_id", "review_type", "summary"],
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
// Discovery Tools
|
|
707
|
+
{
|
|
708
|
+
name: "list_routes",
|
|
709
|
+
description: "List all available API routes on the loopctl server.",
|
|
710
|
+
inputSchema: {
|
|
711
|
+
type: "object",
|
|
712
|
+
properties: {},
|
|
713
|
+
required: [],
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
// MCP Server
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
|
|
722
|
+
const server = new Server(
|
|
723
|
+
{
|
|
724
|
+
name: "loopctl",
|
|
725
|
+
version: "1.0.0",
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
capabilities: { tools: {} },
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
733
|
+
tools: TOOLS,
|
|
734
|
+
}));
|
|
735
|
+
|
|
736
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
737
|
+
const { name, arguments: args } = request.params;
|
|
738
|
+
|
|
739
|
+
switch (name) {
|
|
740
|
+
// Project Tools
|
|
741
|
+
case "get_tenant":
|
|
742
|
+
return await getTenant();
|
|
743
|
+
|
|
744
|
+
case "list_projects":
|
|
745
|
+
return await listProjects();
|
|
746
|
+
|
|
747
|
+
case "create_project":
|
|
748
|
+
return await createProject(args);
|
|
749
|
+
|
|
750
|
+
case "get_progress":
|
|
751
|
+
return await getProgress(args);
|
|
752
|
+
|
|
753
|
+
case "import_stories":
|
|
754
|
+
return await importStories(args);
|
|
755
|
+
|
|
756
|
+
// Story Tools
|
|
757
|
+
case "list_stories":
|
|
758
|
+
return await listStories(args);
|
|
759
|
+
|
|
760
|
+
case "list_ready_stories":
|
|
761
|
+
return await listReadyStories(args);
|
|
762
|
+
|
|
763
|
+
case "get_story":
|
|
764
|
+
return await getStory(args);
|
|
765
|
+
|
|
766
|
+
// Workflow Tools
|
|
767
|
+
case "contract_story":
|
|
768
|
+
return await contractStory(args);
|
|
769
|
+
|
|
770
|
+
case "claim_story":
|
|
771
|
+
return await claimStory(args);
|
|
772
|
+
|
|
773
|
+
case "start_story":
|
|
774
|
+
return await startStory(args);
|
|
775
|
+
|
|
776
|
+
case "request_review":
|
|
777
|
+
return await requestReview(args);
|
|
778
|
+
|
|
779
|
+
// Reviewer Tools
|
|
780
|
+
case "report_story":
|
|
781
|
+
return await reportStory(args);
|
|
782
|
+
|
|
783
|
+
case "review_complete":
|
|
784
|
+
return await reviewComplete(args);
|
|
785
|
+
|
|
786
|
+
// Verification Tools
|
|
787
|
+
case "verify_story":
|
|
788
|
+
return await verifyStory(args);
|
|
789
|
+
|
|
790
|
+
case "reject_story":
|
|
791
|
+
return await rejectStory(args);
|
|
792
|
+
|
|
793
|
+
// Bulk Tools
|
|
794
|
+
case "bulk_mark_complete":
|
|
795
|
+
return await bulkMarkComplete(args);
|
|
796
|
+
|
|
797
|
+
case "verify_all_in_epic":
|
|
798
|
+
return await verifyAllInEpic(args);
|
|
799
|
+
|
|
800
|
+
// Discovery Tools
|
|
801
|
+
case "list_routes":
|
|
802
|
+
return await listRoutes();
|
|
803
|
+
|
|
804
|
+
default:
|
|
805
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
// Start
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
const transport = new StdioServerTransport();
|
|
814
|
+
await server.connect(transport).catch((err) => {
|
|
815
|
+
process.stderr.write(`loopctl MCP server failed to start: ${err.message}\n`);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "loopctl-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for loopctl — structural trust for AI development loops",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"loopctl-mcp-server": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"loopctl",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"ai-development"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/mkreyman/loopctl.git",
|
|
24
|
+
"directory": "mcp-server"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://loopctl.com",
|
|
27
|
+
"author": "Mark Kreyman",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"exports": "./index.js",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.28.0"
|
|
40
|
+
}
|
|
41
|
+
}
|