@ign8t/mcp 0.1.2 → 0.3.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/README.md +76 -48
- package/dist/index.js +512 -38
- package/dist/workspace-snapshot.js +197 -0
- package/package.json +17 -21
package/README.md
CHANGED
|
@@ -7,11 +7,14 @@ Seamlessly integrate ign8t project management into your AI-powered development w
|
|
|
7
7
|
Model Context Protocol (MCP) is an open standard that enables AI assistants to securely access external tools and data sources. With the ign8t MCP server, your AI assistant can:
|
|
8
8
|
|
|
9
9
|
- 📋 Access all your ign8t projects without switching contexts
|
|
10
|
-
- 📄 Read
|
|
10
|
+
- 📄 Read master specs, feature specs, legacy PRP/BRD/PRD/TRD documents, and task specs
|
|
11
11
|
- 📊 View and manage your backlog in real-time
|
|
12
|
+
- 🧩 See which feature specs exist and which tasks still need specs
|
|
12
13
|
- ✅ Update task statuses as you complete work
|
|
13
14
|
- 🔍 Search across all tasks and projects
|
|
14
|
-
- 🎯
|
|
15
|
+
- 🎯 Review, accept, or reject AI/backlog recommendations
|
|
16
|
+
- ➕ Add newly discovered tasks or feature requests from your IDE
|
|
17
|
+
- 📥 Import your local codebase into the project context
|
|
15
18
|
- 🧠 Access full context including related requirements and documentation
|
|
16
19
|
|
|
17
20
|
## Quick Start
|
|
@@ -24,7 +27,7 @@ pnpm install -g @ign8t/mcp
|
|
|
24
27
|
|
|
25
28
|
### 2. Get Your API Key
|
|
26
29
|
|
|
27
|
-
1. Log in to [ign8t.com](https://ign8t.com)
|
|
30
|
+
1. Log in to [ign8t.com](https://www.ign8t.com)
|
|
28
31
|
2. Navigate to **Settings → API Keys**
|
|
29
32
|
3. Click **"Create New API Key"**
|
|
30
33
|
4. Give it a descriptive name (e.g., "Cursor Integration")
|
|
@@ -35,7 +38,7 @@ pnpm install -g @ign8t/mcp
|
|
|
35
38
|
<details>
|
|
36
39
|
<summary><strong>Cursor IDE (Recommended - using npx)</strong></summary>
|
|
37
40
|
|
|
38
|
-
Add to `~/.cursor/mcp
|
|
41
|
+
Add to `~/.cursor/mcp.json`:
|
|
39
42
|
|
|
40
43
|
```json
|
|
41
44
|
{
|
|
@@ -140,8 +143,18 @@ Once configured, you can interact with ign8t naturally through your AI assistant
|
|
|
140
143
|
```
|
|
141
144
|
"Show me all my active projects"
|
|
142
145
|
"Get details about the [project name] project"
|
|
143
|
-
"Show me the
|
|
146
|
+
"Show me the master spec for [project name]"
|
|
144
147
|
"Display all documents for my current project"
|
|
148
|
+
"Import this codebase into project [id]"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Specs Workflow
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
"What specs are left for this project?"
|
|
155
|
+
"List the master spec, generated task specs, and missing specs"
|
|
156
|
+
"Show me the feature spec for the authentication task"
|
|
157
|
+
"Get all task specs for project [id]"
|
|
145
158
|
```
|
|
146
159
|
|
|
147
160
|
### Task Workflow
|
|
@@ -153,6 +166,16 @@ Once configured, you can interact with ign8t naturally through your AI assistant
|
|
|
153
166
|
"Update task [id] to in_progress"
|
|
154
167
|
"Mark task [id] as done with note: 'Implemented OAuth2'"
|
|
155
168
|
"Get full context for task [id]"
|
|
169
|
+
"Add a task for rate limiting the public API"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Recommendation Workflow
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
"I want to add Stripe billing next"
|
|
176
|
+
"List pending recommendations"
|
|
177
|
+
"Accept recommendation [id] and create a task"
|
|
178
|
+
"Reject recommendation [id]"
|
|
156
179
|
```
|
|
157
180
|
|
|
158
181
|
### Resources
|
|
@@ -164,26 +187,57 @@ The MCP server also provides quick access to resources:
|
|
|
164
187
|
|
|
165
188
|
## Available Tools Reference
|
|
166
189
|
|
|
167
|
-
| Tool
|
|
168
|
-
|
|
|
169
|
-
| `list_projects`
|
|
170
|
-
| `get_project`
|
|
171
|
-
| `get_documents`
|
|
172
|
-
| `get_backlog`
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
190
|
+
| Tool | Description | Example Use |
|
|
191
|
+
| ------------------------ | ------------------------------------------------ | -------------------------------------------- |
|
|
192
|
+
| `list_projects` | List all projects with optional status filter | "Show my active projects" |
|
|
193
|
+
| `get_project` | Get detailed project information | "Get details for project abc123" |
|
|
194
|
+
| `get_documents` | Retrieve master spec, task specs, and legacy documents | "Show me the master spec for project xyz" |
|
|
195
|
+
| `get_backlog` | View epics, stories, and tasks | "Display backlog including completed items" |
|
|
196
|
+
| `get_specs` | List master, generated, and missing task specs | "What specs are left?" |
|
|
197
|
+
| `get_next_task` | Next todo task (highest priority, then oldest) plus task spec preview | "What should I work on next?" |
|
|
198
|
+
| `create_task` | Add a concrete task directly to backlog | "Add a task for OAuth callback handling" |
|
|
199
|
+
| `update_task_status` | Change task status with notes | "Set task 123 to blocked: waiting on API" |
|
|
200
|
+
| `search_tasks` | Search across tasks | "Find all tasks mentioning 'payment'" |
|
|
201
|
+
| `get_task_context` | Full task context with requirements | "Show context for task 456" |
|
|
202
|
+
| `add_feature_request` | Queue a feature idea as a backlog recommendation | "I want to add team invites next" |
|
|
203
|
+
| `list_recommendations` | List queued backlog recommendations | "Show pending feature recommendations" |
|
|
204
|
+
| `accept_recommendations` | Convert recommendations into tasks | "Accept recommendations abc and def" |
|
|
205
|
+
| `reject_recommendations` | Reject recommendations | "Reject this duplicate recommendation" |
|
|
206
|
+
| `add_user_persona` | Add or replace a user persona in context | "Add admin operator persona" |
|
|
207
|
+
| `update_success_criteria`| Add or update measurable success criteria | "Add activation-rate target" |
|
|
208
|
+
| `update_context` | Change the tech stack and constraints after you confirm the exact change | "We moved from Firebase to Supabase" |
|
|
209
|
+
| `import_project` | Import the local codebase into project context (28 Sparks) | "Import this codebase into project abc123" |
|
|
210
|
+
|
|
211
|
+
The write tools (`create_task`, `update_task_status`, `accept_recommendations`, `reject_recommendations`, `add_feature_request`, `add_user_persona`, `update_success_criteria`, `update_context`) are free. Only `import_project` costs Sparks.
|
|
212
|
+
|
|
213
|
+
### Updating the implementation context
|
|
214
|
+
|
|
215
|
+
`update_context` lets the project owner's AI change what ign8t knows about the implementation: the tech stack's platform and framework (set or unset), its databases and AI models, and the project constraints (add or remove items: added values are trimmed, removed values must match the stored text exactly). Vision, features, users and success criteria are not writable through it.
|
|
216
|
+
|
|
217
|
+
- The AI reads the project with `get_project` first, shows you the exact change, and applies it only after you agree. The change is saved immediately and costs no Sparks.
|
|
218
|
+
- All previously generated documents (BRD/PRD/TRD, delivery brief, master spec, task specs) may be marked stale. They are not regenerated automatically, and regenerating them costs Sparks.
|
|
219
|
+
- If the context changed since the AI read it, nothing is saved: the AI reads it again and asks you again.
|
|
220
|
+
|
|
221
|
+
### Importing a local codebase
|
|
222
|
+
|
|
223
|
+
`import_project` takes the project ID and the **absolute** path of your project folder, and fills the project's context (vision, users, features, tech stack) using ign8t's AI extraction. Results are written in Korean by default; pass `locale: "en"` for English.
|
|
224
|
+
|
|
225
|
+
- **Cost:** 28 Sparks per new import. Re-importing the same content (README, package.json name, description, script and dependency names, notable paths, primary language) is free as long as nothing else has changed the project context since that import; after an edit in ign8t or another import, it runs (and is charged) again so the context is restored. When the import reports a failure, nothing is saved and the Sparks are refunded; the error's `refund_status` says when a refund needs manual follow-up. If the connection drops before any answer arrives, check the project in ign8t before retrying: a retry of an import that was already saved is free, but one whose charge went through without a save is charged again.
|
|
226
|
+
- **What is uploaded:** the README (first 6,000 characters), the `package.json` name (or the folder name when it has none), description, script names and dependency names, up to 120 notable file paths (for example `src/`, `app/`, `supabase/`), the file count and the primary language guessed from file extensions. Nothing else: no other file contents and no environment values.
|
|
227
|
+
- **What is skipped:** hidden folders (`.git`, `.next`, `.venv`, ...), `node_modules`, build output (`dist`, `build`, `out`, ...), `vendor`, `Pods`, Python virtual environments (any folder with `pyvenv.cfg`), folders nested more than 12 levels deep, and folders you cannot read (listed in the result). The walk stops after 20,000 files and says so. Symbolic links inside the folder are not followed.
|
|
177
228
|
|
|
178
229
|
## Workflow Integration
|
|
179
230
|
|
|
180
231
|
The ign8t MCP server is designed to fit seamlessly into your development workflow:
|
|
181
232
|
|
|
182
233
|
1. **Start your day**: "What are my current tasks?"
|
|
183
|
-
2. **
|
|
184
|
-
3. **
|
|
185
|
-
4. **
|
|
186
|
-
5. **
|
|
234
|
+
2. **Find the next build unit**: "What task should I work on next?"
|
|
235
|
+
3. **Check spec coverage**: "What specs are left?"
|
|
236
|
+
4. **Get context**: "Show me the task context and master spec"
|
|
237
|
+
5. **Add discovered work**: "Queue a recommendation for webhook retry support"
|
|
238
|
+
6. **Accept planned work**: "Accept that recommendation and create a task"
|
|
239
|
+
7. **Update progress**: "Mark this task as in progress"
|
|
240
|
+
8. **Complete work**: "Mark task as done and get my next task"
|
|
187
241
|
|
|
188
242
|
## Troubleshooting
|
|
189
243
|
|
|
@@ -218,22 +272,20 @@ The ign8t MCP server is designed to fit seamlessly into your development workflo
|
|
|
218
272
|
|
|
219
273
|
### Custom API Endpoint (Optional)
|
|
220
274
|
|
|
221
|
-
The MCP server connects to `https://ign8t.com` by default. For enterprise deployments, self-hosted instances, or local development, you can override the API endpoint
|
|
222
|
-
|
|
223
|
-
**Using npx (Recommended):**
|
|
275
|
+
The MCP server connects to `https://www.ign8t.com` by default. For enterprise deployments, self-hosted instances, or local development, you can override the API endpoint with `--api-url`:
|
|
224
276
|
|
|
225
277
|
```json
|
|
226
278
|
{
|
|
227
279
|
"mcpServers": {
|
|
228
280
|
"ign8t": {
|
|
229
281
|
"command": "npx",
|
|
230
|
-
"args": ["-y", "@ign8t/mcp@latest", "--api-key", "your-api-key"]
|
|
282
|
+
"args": ["-y", "@ign8t/mcp@latest", "--api-key", "your-api-key", "--api-url", "http://localhost:3000"]
|
|
231
283
|
}
|
|
232
284
|
}
|
|
233
285
|
}
|
|
234
286
|
```
|
|
235
287
|
|
|
236
|
-
|
|
288
|
+
Or set `IGN8T_API_URL` in the `env` block of the same IDE configuration. A `.env` file in your workspace is not read.
|
|
237
289
|
|
|
238
290
|
**Note**: Most users don't need to set a custom API URL. It's only required if you're:
|
|
239
291
|
|
|
@@ -250,32 +302,8 @@ For contributors working on the MCP server itself:
|
|
|
250
302
|
3. Build the project: `pnpm build`
|
|
251
303
|
4. Test locally: `pnpm dev --api-key your-key`
|
|
252
304
|
|
|
253
|
-
## Contributing
|
|
254
|
-
|
|
255
|
-
We welcome contributions! If you'd like to contribute to the ign8t MCP server:
|
|
256
|
-
|
|
257
|
-
### Development Setup
|
|
258
|
-
|
|
259
|
-
```bash
|
|
260
|
-
# Clone the repository
|
|
261
|
-
git clone https://github.com/ign8t/mcp-server.git
|
|
262
|
-
cd mcp-server
|
|
263
|
-
|
|
264
|
-
# Install dependencies
|
|
265
|
-
pnpm install
|
|
266
|
-
|
|
267
|
-
# Run tests
|
|
268
|
-
pnpm test
|
|
269
|
-
|
|
270
|
-
# Build
|
|
271
|
-
pnpm build
|
|
272
|
-
````
|
|
273
|
-
|
|
274
305
|
## Support
|
|
275
306
|
|
|
276
|
-
- 📚 **Documentation**: [docs.ign8t.com/mcp](https://docs.ign8t.com/mcp)
|
|
277
|
-
- 🐛 **Issues**: [GitHub Issues](https://github.com/ign8t/mcp-server/issues)
|
|
278
|
-
- 💬 **Community**: [Discord](https://discord.gg/ign8t)
|
|
279
307
|
- 📧 **Email**: support@ign8t.com
|
|
280
308
|
|
|
281
309
|
## License
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
dotenv.config();
|
|
5
|
+
import { buildWorkspaceSnapshot } from "./workspace-snapshot.js";
|
|
6
|
+
const VERSION = "0.3.0";
|
|
8
7
|
// Parse command line arguments
|
|
9
8
|
function parseArgs() {
|
|
10
9
|
const args = process.argv.slice(2);
|
|
@@ -28,10 +27,10 @@ Usage: ign8t-mcp --api-key <key> [--api-url <url>]
|
|
|
28
27
|
|
|
29
28
|
Options:
|
|
30
29
|
--api-key <key> Your ign8t API key (required)
|
|
31
|
-
--api-url <url> Custom API URL (optional, defaults to https://ign8t.com)
|
|
30
|
+
--api-url <url> Custom API URL (optional, defaults to https://www.ign8t.com)
|
|
32
31
|
--help, -h Show this help message
|
|
33
32
|
|
|
34
|
-
Get your API key from: https://ign8t.com/settings/api-keys
|
|
33
|
+
Get your API key from: https://www.ign8t.com/settings/api-keys
|
|
35
34
|
`);
|
|
36
35
|
process.exit(0);
|
|
37
36
|
break;
|
|
@@ -41,19 +40,19 @@ Get your API key from: https://ign8t.com/settings/api-keys
|
|
|
41
40
|
}
|
|
42
41
|
// Configuration
|
|
43
42
|
const config = parseArgs();
|
|
44
|
-
const API_KEY = config.apiKey;
|
|
45
|
-
const API_URL = config.apiUrl || "https://ign8t.com";
|
|
43
|
+
const API_KEY = config.apiKey || process.env.IGN8T_API_KEY;
|
|
44
|
+
const API_URL = config.apiUrl || process.env.IGN8T_API_URL || "https://www.ign8t.com";
|
|
46
45
|
if (!API_KEY) {
|
|
47
46
|
console.error("Error: API key is required");
|
|
48
47
|
console.error("Usage: ign8t-mcp --api-key <your-api-key>");
|
|
49
48
|
console.error("Or set IGN8T_API_KEY environment variable");
|
|
50
|
-
console.error("Get your API key from https://ign8t.com/settings/api-keys");
|
|
49
|
+
console.error("Get your API key from https://www.ign8t.com/settings/api-keys");
|
|
51
50
|
process.exit(1);
|
|
52
51
|
}
|
|
53
52
|
// Create MCP server instance
|
|
54
53
|
const server = new Server({
|
|
55
54
|
name: "ign8t-mcp",
|
|
56
|
-
version:
|
|
55
|
+
version: VERSION,
|
|
57
56
|
description: "ign8t project management integration for AI IDEs",
|
|
58
57
|
author: "nocodecat",
|
|
59
58
|
homepage: "https://www.ign8t.com",
|
|
@@ -69,30 +68,37 @@ const server = new Server({
|
|
|
69
68
|
logging: {},
|
|
70
69
|
},
|
|
71
70
|
});
|
|
71
|
+
// Turn a non-2xx response into a message the calling AI can act on: the
|
|
72
|
+
// server's `error` plus its structured fields (error_code, refund_status, ...).
|
|
73
|
+
async function describeApiError(response) {
|
|
74
|
+
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
75
|
+
return `${(await response.text()) || "API error"} (HTTP ${response.status})`;
|
|
76
|
+
}
|
|
77
|
+
const { success: _success, error, ...details } = await response.json();
|
|
78
|
+
const message = `${typeof error === "string" ? error : "API error"} (HTTP ${response.status})`;
|
|
79
|
+
return Object.keys(details).length ? `${message}\n${JSON.stringify(details, null, 2)}` : message;
|
|
80
|
+
}
|
|
72
81
|
// Helper function to call ign8t API
|
|
73
82
|
async function callIgn8tAPI(endpoint, data = {}) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const error = await response.text();
|
|
85
|
-
throw new Error(error || `API error: ${response.status}`);
|
|
86
|
-
}
|
|
87
|
-
return response.json();
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
if (error instanceof Error) {
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
throw new Error("Failed to connect to ign8t API");
|
|
83
|
+
const response = await fetch(`${API_URL}/api/mcp${endpoint}`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"X-API-Key": API_KEY,
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(data),
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(await describeApiError(response));
|
|
94
93
|
}
|
|
94
|
+
return response.json();
|
|
95
95
|
}
|
|
96
|
+
// update_context's add/remove of list items, each at most maxLength characters; the
|
|
97
|
+
// 20-operation cap is on the whole patch (see its description).
|
|
98
|
+
const listOps = (maxLength) => {
|
|
99
|
+
const items = { type: "array", items: { type: "string", minLength: 1, maxLength } };
|
|
100
|
+
return { type: "object", properties: { add: items, remove: items }, additionalProperties: false };
|
|
101
|
+
};
|
|
96
102
|
// Define available tools
|
|
97
103
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
98
104
|
return {
|
|
@@ -120,6 +126,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
120
126
|
properties: {
|
|
121
127
|
project_id: {
|
|
122
128
|
type: "string",
|
|
129
|
+
format: "uuid",
|
|
123
130
|
description: "The project ID",
|
|
124
131
|
},
|
|
125
132
|
},
|
|
@@ -128,17 +135,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
128
135
|
},
|
|
129
136
|
{
|
|
130
137
|
name: "get_documents",
|
|
131
|
-
description: "Get
|
|
138
|
+
description: "Get master specs, task specs, and legacy BRD/PRD/TRD documents for a project",
|
|
132
139
|
inputSchema: {
|
|
133
140
|
type: "object",
|
|
134
141
|
properties: {
|
|
135
142
|
project_id: {
|
|
136
143
|
type: "string",
|
|
144
|
+
format: "uuid",
|
|
137
145
|
description: "The project ID",
|
|
138
146
|
},
|
|
139
147
|
document_type: {
|
|
140
148
|
type: "string",
|
|
141
|
-
enum: ["brd", "prd", "trd", "all"],
|
|
149
|
+
enum: ["master_spec", "prp", "brd", "prd", "trd", "task_spec", "all"],
|
|
142
150
|
description: "Type of document to retrieve",
|
|
143
151
|
default: "all",
|
|
144
152
|
},
|
|
@@ -154,6 +162,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
154
162
|
properties: {
|
|
155
163
|
project_id: {
|
|
156
164
|
type: "string",
|
|
165
|
+
format: "uuid",
|
|
157
166
|
description: "The project ID",
|
|
158
167
|
},
|
|
159
168
|
include_completed: {
|
|
@@ -166,18 +175,154 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
166
175
|
},
|
|
167
176
|
},
|
|
168
177
|
{
|
|
169
|
-
name: "
|
|
170
|
-
description: "
|
|
178
|
+
name: "get_specs",
|
|
179
|
+
description: "List the master spec, generated feature specs, and tasks that still need specs for a project. Use this to answer what specs are left.",
|
|
171
180
|
inputSchema: {
|
|
172
181
|
type: "object",
|
|
173
182
|
properties: {
|
|
174
183
|
project_id: {
|
|
175
184
|
type: "string",
|
|
185
|
+
format: "uuid",
|
|
176
186
|
description: "The project ID",
|
|
177
187
|
},
|
|
178
|
-
|
|
188
|
+
include_generated: {
|
|
189
|
+
type: "boolean",
|
|
190
|
+
description: "Include specs that already exist",
|
|
191
|
+
default: true,
|
|
192
|
+
},
|
|
193
|
+
include_missing: {
|
|
194
|
+
type: "boolean",
|
|
195
|
+
description: "Include backlog tasks without generated specs",
|
|
196
|
+
default: true,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
required: ["project_id"],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "create_task",
|
|
204
|
+
description: "Add a new implementation task to a project's backlog from the IDE. Use this when you discover missing work or the user asks to add a feature/task.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {
|
|
208
|
+
project_id: {
|
|
209
|
+
type: "string",
|
|
210
|
+
format: "uuid",
|
|
211
|
+
description: "The project ID",
|
|
212
|
+
},
|
|
213
|
+
title: {
|
|
214
|
+
type: "string",
|
|
215
|
+
minLength: 3,
|
|
216
|
+
maxLength: 200,
|
|
217
|
+
description: "Short task title",
|
|
218
|
+
},
|
|
219
|
+
description: {
|
|
179
220
|
type: "string",
|
|
180
|
-
|
|
221
|
+
maxLength: 10000,
|
|
222
|
+
description: "Detailed task description",
|
|
223
|
+
},
|
|
224
|
+
priority: {
|
|
225
|
+
type: "string",
|
|
226
|
+
enum: ["critical", "high", "medium", "low"],
|
|
227
|
+
description: "Task priority",
|
|
228
|
+
default: "medium",
|
|
229
|
+
},
|
|
230
|
+
status: {
|
|
231
|
+
type: "string",
|
|
232
|
+
enum: ["todo", "in_progress", "review", "done", "blocked"],
|
|
233
|
+
description: "Initial task status",
|
|
234
|
+
default: "todo",
|
|
235
|
+
},
|
|
236
|
+
acceptance_criteria: {
|
|
237
|
+
type: "array",
|
|
238
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
239
|
+
maxItems: 30,
|
|
240
|
+
description: "Acceptance criteria for the task",
|
|
241
|
+
},
|
|
242
|
+
technical_details: {
|
|
243
|
+
type: "string",
|
|
244
|
+
maxLength: 10000,
|
|
245
|
+
description: "Implementation notes or technical requirements",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
required: ["project_id", "title"],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "list_recommendations",
|
|
253
|
+
description: "List backlog recommendations queued for review. Use this after adding feature requests or when deciding what to build next.",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
project_id: {
|
|
258
|
+
type: "string",
|
|
259
|
+
format: "uuid",
|
|
260
|
+
description: "The project ID",
|
|
261
|
+
},
|
|
262
|
+
status: {
|
|
263
|
+
type: "string",
|
|
264
|
+
enum: ["pending", "accepted", "rejected", "expired", "all"],
|
|
265
|
+
description: "Recommendation status to list",
|
|
266
|
+
default: "pending",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
required: ["project_id"],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "accept_recommendations",
|
|
274
|
+
description: "Accept pending backlog recommendations and create concrete tasks from them.",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
project_id: {
|
|
279
|
+
type: "string",
|
|
280
|
+
format: "uuid",
|
|
281
|
+
description: "The project ID",
|
|
282
|
+
},
|
|
283
|
+
recommendation_ids: {
|
|
284
|
+
type: "array",
|
|
285
|
+
items: { type: "string", format: "uuid" },
|
|
286
|
+
minItems: 1,
|
|
287
|
+
maxItems: 50,
|
|
288
|
+
description: "Recommendation IDs to accept",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
required: ["project_id", "recommendation_ids"],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "reject_recommendations",
|
|
296
|
+
description: "Reject pending backlog recommendations so they no longer appear as work candidates.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
project_id: {
|
|
301
|
+
type: "string",
|
|
302
|
+
format: "uuid",
|
|
303
|
+
description: "The project ID",
|
|
304
|
+
},
|
|
305
|
+
recommendation_ids: {
|
|
306
|
+
type: "array",
|
|
307
|
+
items: { type: "string", format: "uuid" },
|
|
308
|
+
minItems: 1,
|
|
309
|
+
maxItems: 50,
|
|
310
|
+
description: "Recommendation IDs to reject",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
required: ["project_id", "recommendation_ids"],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "get_next_task",
|
|
318
|
+
description: "Get the next todo task to work on (highest priority, then oldest), including its generated task spec when available.",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
project_id: {
|
|
323
|
+
type: "string",
|
|
324
|
+
format: "uuid",
|
|
325
|
+
description: "The project ID",
|
|
181
326
|
},
|
|
182
327
|
},
|
|
183
328
|
required: ["project_id"],
|
|
@@ -191,6 +336,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
191
336
|
properties: {
|
|
192
337
|
task_id: {
|
|
193
338
|
type: "string",
|
|
339
|
+
format: "uuid",
|
|
194
340
|
description: "The task ID",
|
|
195
341
|
},
|
|
196
342
|
status: {
|
|
@@ -200,6 +346,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
200
346
|
},
|
|
201
347
|
notes: {
|
|
202
348
|
type: "string",
|
|
349
|
+
maxLength: 10000,
|
|
203
350
|
description: "Optional notes about the status change",
|
|
204
351
|
},
|
|
205
352
|
},
|
|
@@ -218,6 +365,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
218
365
|
},
|
|
219
366
|
project_id: {
|
|
220
367
|
type: "string",
|
|
368
|
+
format: "uuid",
|
|
221
369
|
description: "Optional: limit search to specific project",
|
|
222
370
|
},
|
|
223
371
|
status: {
|
|
@@ -232,23 +380,268 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
232
380
|
},
|
|
233
381
|
{
|
|
234
382
|
name: "get_task_context",
|
|
235
|
-
description: "Get
|
|
383
|
+
description: "Get a task with its story, epic, its own task spec and the project's master spec",
|
|
236
384
|
inputSchema: {
|
|
237
385
|
type: "object",
|
|
238
386
|
properties: {
|
|
239
387
|
task_id: {
|
|
240
388
|
type: "string",
|
|
389
|
+
format: "uuid",
|
|
241
390
|
description: "The task ID",
|
|
242
391
|
},
|
|
243
392
|
},
|
|
244
393
|
required: ["task_id"],
|
|
245
394
|
},
|
|
246
395
|
},
|
|
396
|
+
{
|
|
397
|
+
name: "import_project",
|
|
398
|
+
description: "Import the local codebase into the ign8t project's context (AI extraction of product vision, users, features and tech stack). Costs 28 Sparks per new import; re-importing the same content (README, package.json name/description/script/dependency names, notable paths, primary language) is free while nothing else has changed the project context since the last import. Uploads only: the README (first 6000 characters), the package.json name (or the folder name when it has none), description, script names and dependency names, up to 120 notable file paths, the file count and the primary language guessed from file extensions — never other file contents or environment values.",
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
project_id: {
|
|
403
|
+
type: "string",
|
|
404
|
+
format: "uuid",
|
|
405
|
+
description: "The ign8t project ID to import into",
|
|
406
|
+
},
|
|
407
|
+
workspace_path: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "Absolute path to the project root directory",
|
|
410
|
+
},
|
|
411
|
+
locale: {
|
|
412
|
+
type: "string",
|
|
413
|
+
enum: ["ko", "en"],
|
|
414
|
+
description: "Language of the extracted context",
|
|
415
|
+
default: "ko",
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
required: ["project_id", "workspace_path"],
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
// ===== BIDIRECTIONAL CONTEXT UPDATE TOOLS =====
|
|
422
|
+
{
|
|
423
|
+
name: "add_feature_request",
|
|
424
|
+
description: "Request a new feature spec candidate discovered during development. This queues the idea for product-owner review before implementation.",
|
|
425
|
+
inputSchema: {
|
|
426
|
+
type: "object",
|
|
427
|
+
properties: {
|
|
428
|
+
project_id: {
|
|
429
|
+
type: "string",
|
|
430
|
+
format: "uuid",
|
|
431
|
+
description: "The project ID",
|
|
432
|
+
},
|
|
433
|
+
feature: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
name: {
|
|
437
|
+
type: "string",
|
|
438
|
+
minLength: 1,
|
|
439
|
+
maxLength: 200,
|
|
440
|
+
description: "Feature name (e.g., 'OAuth Social Login')",
|
|
441
|
+
},
|
|
442
|
+
description: {
|
|
443
|
+
type: "string",
|
|
444
|
+
minLength: 1,
|
|
445
|
+
maxLength: 5000,
|
|
446
|
+
description: "Detailed feature description",
|
|
447
|
+
},
|
|
448
|
+
user_story: {
|
|
449
|
+
type: "string",
|
|
450
|
+
minLength: 1,
|
|
451
|
+
maxLength: 2000,
|
|
452
|
+
description: "User story (As a... I want... So that...)",
|
|
453
|
+
},
|
|
454
|
+
priority: {
|
|
455
|
+
type: "string",
|
|
456
|
+
enum: ["critical", "high", "medium", "low"],
|
|
457
|
+
description: "Feature priority",
|
|
458
|
+
},
|
|
459
|
+
estimated_complexity: {
|
|
460
|
+
type: "string",
|
|
461
|
+
enum: ["simple", "moderate", "complex"],
|
|
462
|
+
description: "Estimated implementation complexity",
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
required: ["name", "description", "user_story"],
|
|
466
|
+
},
|
|
467
|
+
related_context: {
|
|
468
|
+
type: "object",
|
|
469
|
+
description: "Additional context for spec definition (optional, at most 10000 characters as JSON)",
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
required: ["project_id", "feature"],
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "add_user_persona",
|
|
477
|
+
description: "Add a new user persona discovered during development. Enriches the project context with detailed user information. Adding a persona with an existing name replaces that persona.",
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: "object",
|
|
480
|
+
properties: {
|
|
481
|
+
project_id: {
|
|
482
|
+
type: "string",
|
|
483
|
+
format: "uuid",
|
|
484
|
+
description: "The project ID",
|
|
485
|
+
},
|
|
486
|
+
persona: {
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: {
|
|
489
|
+
name: {
|
|
490
|
+
type: "string",
|
|
491
|
+
minLength: 1,
|
|
492
|
+
maxLength: 100,
|
|
493
|
+
description: "Persona name (e.g., 'Sarah - Operations Manager')",
|
|
494
|
+
},
|
|
495
|
+
role: {
|
|
496
|
+
type: "string",
|
|
497
|
+
minLength: 1,
|
|
498
|
+
maxLength: 200,
|
|
499
|
+
description: "User role or job title",
|
|
500
|
+
},
|
|
501
|
+
demographics: {
|
|
502
|
+
type: "object",
|
|
503
|
+
properties: {
|
|
504
|
+
age: { type: "string", maxLength: 100 },
|
|
505
|
+
tech_savviness: {
|
|
506
|
+
type: "string",
|
|
507
|
+
enum: ["beginner", "intermediate", "advanced"],
|
|
508
|
+
},
|
|
509
|
+
industry: { type: "string", maxLength: 100 },
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
goals: {
|
|
513
|
+
type: "array",
|
|
514
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
515
|
+
maxItems: 20,
|
|
516
|
+
description: "User goals and objectives",
|
|
517
|
+
},
|
|
518
|
+
pain_points: {
|
|
519
|
+
type: "array",
|
|
520
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
521
|
+
maxItems: 20,
|
|
522
|
+
description: "User pain points and frustrations",
|
|
523
|
+
},
|
|
524
|
+
use_cases: {
|
|
525
|
+
type: "array",
|
|
526
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
527
|
+
maxItems: 20,
|
|
528
|
+
description: "Specific use cases for this persona",
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
required: ["name", "role", "goals", "pain_points"],
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
required: ["project_id", "persona"],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "update_success_criteria",
|
|
539
|
+
description: "Add or update success criteria and metrics for the project. Helps define how success will be measured. A criterion with an existing metric name replaces that criterion.",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: "object",
|
|
542
|
+
properties: {
|
|
543
|
+
project_id: {
|
|
544
|
+
type: "string",
|
|
545
|
+
format: "uuid",
|
|
546
|
+
description: "The project ID",
|
|
547
|
+
},
|
|
548
|
+
criteria: {
|
|
549
|
+
type: "array",
|
|
550
|
+
minItems: 1,
|
|
551
|
+
maxItems: 20,
|
|
552
|
+
items: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {
|
|
555
|
+
metric: {
|
|
556
|
+
type: "string",
|
|
557
|
+
minLength: 1,
|
|
558
|
+
maxLength: 200,
|
|
559
|
+
pattern: "^[^:]*$",
|
|
560
|
+
description: "What to measure, without ':' (e.g., 'User sign-up rate')",
|
|
561
|
+
},
|
|
562
|
+
target: {
|
|
563
|
+
type: "string",
|
|
564
|
+
minLength: 1,
|
|
565
|
+
maxLength: 200,
|
|
566
|
+
description: "Target value (e.g., '80% of visitors')",
|
|
567
|
+
},
|
|
568
|
+
priority: {
|
|
569
|
+
type: "string",
|
|
570
|
+
enum: ["must-have", "nice-to-have"],
|
|
571
|
+
description: "Metric priority",
|
|
572
|
+
},
|
|
573
|
+
measurement_method: {
|
|
574
|
+
type: "string",
|
|
575
|
+
maxLength: 500,
|
|
576
|
+
description: "How this will be measured (optional)",
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
required: ["metric", "target", "priority"],
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
required: ["project_id", "criteria"],
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "update_context",
|
|
588
|
+
description: "Change the project's implementation context: tech_stack platform/framework (set, or list them in tech_stack.unset to clear), tech_stack database/ai_models and constraints (add/remove items: add values are trimmed; remove values must match the stored text exactly). Writes immediately and costs zero Sparks. Call get_project first and pass its context.context_version as expected_context_version. Before calling, show the user the exact change and tell them that all previously generated documents (BRD/PRD/TRD, delivery brief, master spec, task specs) may be marked stale, and that regenerating them costs Sparks; set confirmed_with_user to true only after the user agreed. A CONTEXT_CHANGED error means the context changed since you read it: read it again, reconcile, and ask the user again. Vision, features, users and success criteria are not writable here: use add_user_persona, update_success_criteria or add_feature_request.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
properties: {
|
|
592
|
+
project_id: {
|
|
593
|
+
type: "string",
|
|
594
|
+
format: "uuid",
|
|
595
|
+
description: "The project ID",
|
|
596
|
+
},
|
|
597
|
+
expected_context_version: {
|
|
598
|
+
type: "integer",
|
|
599
|
+
minimum: 1,
|
|
600
|
+
description: "context.context_version from the get_project call the user's confirmation was based on",
|
|
601
|
+
},
|
|
602
|
+
reason: {
|
|
603
|
+
type: "string",
|
|
604
|
+
minLength: 10,
|
|
605
|
+
maxLength: 500,
|
|
606
|
+
description: "Why this change is needed (e.g., what was discovered while implementing)",
|
|
607
|
+
},
|
|
608
|
+
confirmed_with_user: {
|
|
609
|
+
type: "boolean",
|
|
610
|
+
description: "Must be true, set only after the user saw the exact change and agreed to it; anything else is rejected",
|
|
611
|
+
},
|
|
612
|
+
patch: {
|
|
613
|
+
type: "object",
|
|
614
|
+
description: "At most 20 operations in total (each set, unset, added or removed item counts). A list can grow to at most 50 items, and the tech stack and constraints to at most 12000 characters together.",
|
|
615
|
+
properties: {
|
|
616
|
+
tech_stack: {
|
|
617
|
+
type: "object",
|
|
618
|
+
properties: {
|
|
619
|
+
platform: { type: "string", minLength: 1, maxLength: 200 },
|
|
620
|
+
framework: { type: "string", minLength: 1, maxLength: 200 },
|
|
621
|
+
unset: {
|
|
622
|
+
type: "array",
|
|
623
|
+
items: { type: "string", enum: ["platform", "framework"] },
|
|
624
|
+
description: "Keys to clear; a key cannot be both set and unset",
|
|
625
|
+
},
|
|
626
|
+
database: listOps(200),
|
|
627
|
+
ai_models: listOps(200),
|
|
628
|
+
},
|
|
629
|
+
additionalProperties: false,
|
|
630
|
+
},
|
|
631
|
+
constraints: listOps(500),
|
|
632
|
+
},
|
|
633
|
+
additionalProperties: false,
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
required: ["project_id", "expected_context_version", "reason", "confirmed_with_user", "patch"],
|
|
637
|
+
additionalProperties: false,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
247
640
|
],
|
|
248
641
|
};
|
|
249
642
|
});
|
|
250
643
|
// Handle tool calls
|
|
251
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
644
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
252
645
|
const { name, arguments: args } = request.params;
|
|
253
646
|
try {
|
|
254
647
|
let result;
|
|
@@ -265,6 +658,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
265
658
|
case "get_backlog":
|
|
266
659
|
result = await callIgn8tAPI("/backlog/get", args);
|
|
267
660
|
break;
|
|
661
|
+
case "get_specs":
|
|
662
|
+
result = await callIgn8tAPI("/specs/list", args);
|
|
663
|
+
break;
|
|
664
|
+
case "create_task":
|
|
665
|
+
result = await callIgn8tAPI("/tasks/create", args);
|
|
666
|
+
break;
|
|
667
|
+
case "list_recommendations":
|
|
668
|
+
result = await callIgn8tAPI("/recommendations/list", args);
|
|
669
|
+
break;
|
|
670
|
+
case "accept_recommendations":
|
|
671
|
+
result = await callIgn8tAPI("/recommendations/accept", args);
|
|
672
|
+
break;
|
|
673
|
+
case "reject_recommendations":
|
|
674
|
+
result = await callIgn8tAPI("/recommendations/reject", args);
|
|
675
|
+
break;
|
|
268
676
|
case "get_next_task":
|
|
269
677
|
result = await callIgn8tAPI("/tasks/next", args);
|
|
270
678
|
break;
|
|
@@ -277,6 +685,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
277
685
|
case "get_task_context":
|
|
278
686
|
result = await callIgn8tAPI("/tasks/context", args);
|
|
279
687
|
break;
|
|
688
|
+
case "import_project": {
|
|
689
|
+
const { snapshot, unreadableDirectories, fileLimitReached } = await buildWorkspaceSnapshot(args?.workspace_path);
|
|
690
|
+
// The upload is what charges Sparks: a call the user cancelled while the folder
|
|
691
|
+
// was being read must not go on to charge.
|
|
692
|
+
extra.signal.throwIfAborted();
|
|
693
|
+
// The server's reply states what happened: sparks_charged, already_imported, warning.
|
|
694
|
+
result = await callIgn8tAPI("/projects/import", {
|
|
695
|
+
project_id: args?.project_id,
|
|
696
|
+
locale: args?.locale,
|
|
697
|
+
workspace: snapshot,
|
|
698
|
+
});
|
|
699
|
+
if (unreadableDirectories.length) {
|
|
700
|
+
result = { ...result, skipped_unreadable_directories: unreadableDirectories };
|
|
701
|
+
}
|
|
702
|
+
if (fileLimitReached) {
|
|
703
|
+
result = {
|
|
704
|
+
...result,
|
|
705
|
+
file_limit_reached: "The folder has more than 20000 files; the count and notable paths cover the first 20000 reached (folders walked depth-first, each sorted by name).",
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
// ===== BIDIRECTIONAL CONTEXT UPDATE HANDLERS =====
|
|
711
|
+
case "add_feature_request": {
|
|
712
|
+
console.error(`Adding feature request for project: ${args.project_id}`);
|
|
713
|
+
result = await callIgn8tAPI("/features/add", {
|
|
714
|
+
project_id: args.project_id,
|
|
715
|
+
feature: args.feature,
|
|
716
|
+
related_context: args.related_context,
|
|
717
|
+
mcp_metadata: {
|
|
718
|
+
ide: "MCP Client",
|
|
719
|
+
timestamp: new Date().toISOString(),
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
console.error("Feature request submitted successfully");
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "add_user_persona": {
|
|
726
|
+
console.error(`Adding user persona for project: ${args.project_id}`);
|
|
727
|
+
result = await callIgn8tAPI("/personas/add", {
|
|
728
|
+
project_id: args.project_id,
|
|
729
|
+
persona: args.persona,
|
|
730
|
+
mcp_metadata: {
|
|
731
|
+
ide: "MCP Client",
|
|
732
|
+
timestamp: new Date().toISOString(),
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
console.error("User persona added successfully");
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
case "update_success_criteria": {
|
|
739
|
+
console.error(`Updating success criteria for project: ${args.project_id}`);
|
|
740
|
+
result = await callIgn8tAPI("/criteria/update", {
|
|
741
|
+
project_id: args.project_id,
|
|
742
|
+
criteria: args.criteria,
|
|
743
|
+
mcp_metadata: {
|
|
744
|
+
ide: "MCP Client",
|
|
745
|
+
timestamp: new Date().toISOString(),
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
console.error("Success criteria updated successfully");
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case "update_context":
|
|
752
|
+
result = await callIgn8tAPI("/context/update", args);
|
|
753
|
+
break;
|
|
280
754
|
default:
|
|
281
755
|
throw new Error(`Unknown tool: ${name}`);
|
|
282
756
|
}
|
|
@@ -368,7 +842,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
368
842
|
async function main() {
|
|
369
843
|
const transport = new StdioServerTransport();
|
|
370
844
|
await server.connect(transport);
|
|
371
|
-
console.error(
|
|
845
|
+
console.error(`ign8t MCP server v${VERSION}`);
|
|
372
846
|
console.error(`Connected to: ${API_URL}`);
|
|
373
847
|
console.error("Ready to serve requests...");
|
|
374
848
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Snapshot
|
|
3
|
+
*
|
|
4
|
+
* Builds the bounded, deterministic summary of a local codebase that
|
|
5
|
+
* import_project uploads. Only the README excerpt, package.json metadata,
|
|
6
|
+
* notable file paths and counts leave the machine — never other file contents.
|
|
7
|
+
*/
|
|
8
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
// Hidden directories (.git, .next, .venv, .tox, ...) are skipped too: caches there
|
|
11
|
+
// would otherwise fill the file cap before the real code is reached.
|
|
12
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
13
|
+
"node_modules",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
"out",
|
|
17
|
+
"coverage",
|
|
18
|
+
"vendor",
|
|
19
|
+
"target",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
"venv",
|
|
22
|
+
"Pods",
|
|
23
|
+
]);
|
|
24
|
+
// Same selection the web GitHub import uses (import-github.ts).
|
|
25
|
+
const NOTABLE_FILE = /(^|\/)(package\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config|src\/|app\/|pages\/|components\/|supabase\/|prisma\/|drizzle|README|Dockerfile)/i;
|
|
26
|
+
const LANGUAGE_BY_EXTENSION = {
|
|
27
|
+
".ts": "TypeScript",
|
|
28
|
+
".tsx": "TypeScript",
|
|
29
|
+
".js": "JavaScript",
|
|
30
|
+
".jsx": "JavaScript",
|
|
31
|
+
".mjs": "JavaScript",
|
|
32
|
+
".cjs": "JavaScript",
|
|
33
|
+
".py": "Python",
|
|
34
|
+
".go": "Go",
|
|
35
|
+
".rs": "Rust",
|
|
36
|
+
".java": "Java",
|
|
37
|
+
".kt": "Kotlin",
|
|
38
|
+
".swift": "Swift",
|
|
39
|
+
".rb": "Ruby",
|
|
40
|
+
".php": "PHP",
|
|
41
|
+
".cs": "C#",
|
|
42
|
+
".cpp": "C++",
|
|
43
|
+
".c": "C",
|
|
44
|
+
".dart": "Dart",
|
|
45
|
+
".vue": "Vue",
|
|
46
|
+
".svelte": "Svelte",
|
|
47
|
+
".html": "HTML",
|
|
48
|
+
".css": "CSS",
|
|
49
|
+
};
|
|
50
|
+
const MAX_DEPTH = 12;
|
|
51
|
+
const MAX_FILES = 20_000;
|
|
52
|
+
/**
|
|
53
|
+
* Clip to `max` UTF-16 units without leaving half of a surrogate pair. Unpaired
|
|
54
|
+
* surrogates (possible through package.json escapes) become U+FFFD: the server
|
|
55
|
+
* rejects them, since Postgres cannot store them.
|
|
56
|
+
*/
|
|
57
|
+
function clip(value, max) {
|
|
58
|
+
// NUL (possible through "\u0000" escapes) is dropped for the same reason.
|
|
59
|
+
const wellFormed = Buffer.from(value, "utf8").toString("utf8").replace(/\u0000/g, "");
|
|
60
|
+
const clipped = wellFormed.slice(0, max);
|
|
61
|
+
return /[\uD800-\uDBFF]$/.test(clipped) ? clipped.slice(0, -1) : clipped;
|
|
62
|
+
}
|
|
63
|
+
function objectKeys(value) {
|
|
64
|
+
return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
|
|
65
|
+
}
|
|
66
|
+
function byName(a, b) {
|
|
67
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Relative POSIX paths of regular files, in sorted depth-first order.
|
|
71
|
+
* Symlinks are never followed and the walk stops at MAX_FILES. Subdirectories the
|
|
72
|
+
* user cannot read (e.g. a root-owned database volume) are skipped and returned.
|
|
73
|
+
*/
|
|
74
|
+
async function listFiles(root) {
|
|
75
|
+
// Collects one file past the cap, so "more than MAX_FILES" is a fact, not a guess.
|
|
76
|
+
const files = [];
|
|
77
|
+
const unreadable = [];
|
|
78
|
+
async function visit(directory, prefix, depth) {
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = (await readdir(directory, { withFileTypes: true })).sort(byName);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const code = error.code;
|
|
85
|
+
// ENOENT/ENOTDIR: removed or replaced while the walk ran, or a name the OS cannot reopen.
|
|
86
|
+
if (prefix && (code === "EACCES" || code === "EPERM" || code === "ENOENT" || code === "ENOTDIR")) {
|
|
87
|
+
unreadable.push(prefix.slice(0, -1));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
// A Python virtual environment, whatever its folder is called (PEP 405 marker).
|
|
93
|
+
if (prefix && entries.some((entry) => entry.isFile() && entry.name === "pyvenv.cfg"))
|
|
94
|
+
return;
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (files.length > MAX_FILES)
|
|
97
|
+
return;
|
|
98
|
+
const relativePath = prefix + entry.name;
|
|
99
|
+
if (entry.isFile()) {
|
|
100
|
+
files.push(relativePath);
|
|
101
|
+
}
|
|
102
|
+
else if (entry.isDirectory() &&
|
|
103
|
+
depth < MAX_DEPTH &&
|
|
104
|
+
!entry.name.startsWith(".") &&
|
|
105
|
+
!SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
106
|
+
await visit(path.join(directory, entry.name), `${relativePath}/`, depth + 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await visit(root, "", 0);
|
|
111
|
+
return { files: files.slice(0, MAX_FILES), unreadable, fileLimitReached: files.length > MAX_FILES };
|
|
112
|
+
}
|
|
113
|
+
function primaryLanguage(files) {
|
|
114
|
+
const counts = new Map();
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const language = LANGUAGE_BY_EXTENSION[path.posix.extname(file).toLowerCase()];
|
|
117
|
+
if (language)
|
|
118
|
+
counts.set(language, (counts.get(language) ?? 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
return [...counts].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))[0]?.[0];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The snapshot to upload, plus what it left out (reported to the caller, never sent):
|
|
124
|
+
* unreadable subdirectories and whether the file cap stopped the walk.
|
|
125
|
+
*/
|
|
126
|
+
export async function buildWorkspaceSnapshot(root) {
|
|
127
|
+
if (typeof root !== "string" || !path.isAbsolute(root)) {
|
|
128
|
+
throw new Error(`workspace_path must be an absolute directory path, got: ${String(root)}`);
|
|
129
|
+
}
|
|
130
|
+
if (!(await stat(root)).isDirectory()) {
|
|
131
|
+
throw new Error(`workspace_path is not a directory: ${root}`);
|
|
132
|
+
}
|
|
133
|
+
const rootFiles = (await readdir(root, { withFileTypes: true }))
|
|
134
|
+
.filter((entry) => entry.isFile())
|
|
135
|
+
.map((entry) => entry.name)
|
|
136
|
+
.sort();
|
|
137
|
+
let packageJson = {};
|
|
138
|
+
if (rootFiles.includes("package.json")) {
|
|
139
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
140
|
+
const text = await readFile(packageJsonPath, "utf8");
|
|
141
|
+
try {
|
|
142
|
+
// A leading byte-order mark is valid for npm and Node, but not for JSON.parse.
|
|
143
|
+
packageJson = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// The parser's message quotes the file, which may hold script values; only
|
|
147
|
+
// names leave the machine, so the error must not repeat its contents.
|
|
148
|
+
throw new Error(`Cannot parse ${packageJsonPath}: it is not valid JSON`);
|
|
149
|
+
}
|
|
150
|
+
if (!packageJson || typeof packageJson !== "object" || Array.isArray(packageJson)) {
|
|
151
|
+
throw new Error(`${packageJsonPath} is not a JSON object`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const readmeName = ["readme.md", "readme"]
|
|
155
|
+
.map((wanted) => rootFiles.find((name) => name.toLowerCase() === wanted))
|
|
156
|
+
.find(Boolean);
|
|
157
|
+
// NULs (a UTF-16 README read as UTF-8) are dropped by clip(): the server rejects them.
|
|
158
|
+
const readme = readmeName ? await readFile(path.join(root, readmeName), "utf8") : "";
|
|
159
|
+
const { files, unreadable, fileLimitReached } = await listFiles(root);
|
|
160
|
+
// Every value is clipped before the empty checks: a value made only of dropped
|
|
161
|
+
// characters (e.g. NUL) must count as empty, not be sent as "".
|
|
162
|
+
const packageName = clip(typeof packageJson.name === "string" ? packageJson.name : "", 214);
|
|
163
|
+
const description = clip(typeof packageJson.description === "string" ? packageJson.description : "", 1000);
|
|
164
|
+
const readmeExcerpt = clip(readme, 6000);
|
|
165
|
+
const language = primaryLanguage(files);
|
|
166
|
+
const snapshot = {
|
|
167
|
+
name: packageName || clip(path.basename(root), 214),
|
|
168
|
+
...(description ? { description } : {}),
|
|
169
|
+
...(language ? { primary_language: language } : {}),
|
|
170
|
+
total_files: files.length,
|
|
171
|
+
scripts: objectKeys(packageJson.scripts)
|
|
172
|
+
.map((script) => clip(script, 100))
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.slice(0, 50),
|
|
175
|
+
// Runtime dependencies win the 60 slots over dev tooling (same order as the
|
|
176
|
+
// GitHub import), then the list is sorted so key order cannot change it.
|
|
177
|
+
dependencies: [
|
|
178
|
+
...new Set([
|
|
179
|
+
...objectKeys(packageJson.dependencies).sort(),
|
|
180
|
+
...objectKeys(packageJson.devDependencies).sort(),
|
|
181
|
+
]),
|
|
182
|
+
]
|
|
183
|
+
.filter((dependency) => dependency.length <= 214)
|
|
184
|
+
.map((dependency) => clip(dependency, 214))
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.slice(0, 60)
|
|
187
|
+
.sort(),
|
|
188
|
+
...(readmeExcerpt ? { readme_excerpt: readmeExcerpt } : {}),
|
|
189
|
+
// Paths the server would reject (backslash, drive prefix, over 300 chars) are left out.
|
|
190
|
+
notable_files: files
|
|
191
|
+
.filter((file) => file.length <= 300 && !file.includes("\\") && !/^[A-Za-z]:/.test(file))
|
|
192
|
+
.filter((file) => NOTABLE_FILE.test(file))
|
|
193
|
+
.sort()
|
|
194
|
+
.slice(0, 120),
|
|
195
|
+
};
|
|
196
|
+
return { snapshot, unreadableDirectories: unreadable, fileLimitReached };
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ign8t/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for ign8t project management - AI IDE integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,6 +11,19 @@
|
|
|
11
11
|
"dist/",
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
16
|
+
"build:watch": "tsc --watch",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"dev:help": "tsx src/index.ts --help",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"test": "node debug-mcp.js",
|
|
22
|
+
"test:local": "node dist/index.js --help",
|
|
23
|
+
"link:local": "npm link",
|
|
24
|
+
"unlink:local": "npm unlink -g @ign8t/mcp",
|
|
25
|
+
"prepublishOnly": "pnpm clean && pnpm build"
|
|
26
|
+
},
|
|
14
27
|
"keywords": [
|
|
15
28
|
"mcp",
|
|
16
29
|
"model-context-protocol",
|
|
@@ -25,34 +38,17 @@
|
|
|
25
38
|
"mcp": {
|
|
26
39
|
"name": "ign8t-mcp",
|
|
27
40
|
"description": "MCP server for ign8t project management - AI IDE integration",
|
|
28
|
-
"version": "
|
|
41
|
+
"version": "0.3.0"
|
|
29
42
|
},
|
|
30
43
|
"author": "nocodecat",
|
|
31
44
|
"license": "MIT",
|
|
32
45
|
"homepage": "https://www.ign8t.com",
|
|
33
|
-
"repository": {
|
|
34
|
-
"type": "git",
|
|
35
|
-
"url": "https://github.com/ign8t/mcp-server.git"
|
|
36
|
-
},
|
|
37
46
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^0.5.0"
|
|
39
|
-
"dotenv": "^16.3.1"
|
|
47
|
+
"@modelcontextprotocol/sdk": "^0.5.0"
|
|
40
48
|
},
|
|
41
49
|
"devDependencies": {
|
|
42
50
|
"@types/node": "^20.10.5",
|
|
43
51
|
"tsx": "^4.7.0",
|
|
44
52
|
"typescript": "^5.3.3"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "tsc && chmod +x dist/index.js",
|
|
48
|
-
"build:watch": "tsc --watch",
|
|
49
|
-
"start": "node dist/index.js",
|
|
50
|
-
"dev": "tsx src/index.ts",
|
|
51
|
-
"dev:help": "tsx src/index.ts --help",
|
|
52
|
-
"clean": "rm -rf dist",
|
|
53
|
-
"test": "node debug-mcp.js",
|
|
54
|
-
"test:local": "node dist/index.js --help",
|
|
55
|
-
"link:local": "npm link",
|
|
56
|
-
"unlink:local": "npm unlink -g @ign8t/mcp"
|
|
57
53
|
}
|
|
58
|
-
}
|
|
54
|
+
}
|