@ign8t/mcp 0.1.2 → 0.2.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 +67 -48
- package/dist/index.js +449 -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,48 @@ 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
|
+
| `import_project` | Import the local codebase into project context (28 Sparks) | "Import this codebase into project abc123" |
|
|
209
|
+
|
|
210
|
+
The write tools (`create_task`, `update_task_status`, `accept_recommendations`, `reject_recommendations`, `add_feature_request`, `add_user_persona`, `update_success_criteria`) are free. Only `import_project` costs Sparks.
|
|
211
|
+
|
|
212
|
+
### Importing a local codebase
|
|
213
|
+
|
|
214
|
+
`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.
|
|
215
|
+
|
|
216
|
+
- **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.
|
|
217
|
+
- **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.
|
|
218
|
+
- **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
219
|
|
|
178
220
|
## Workflow Integration
|
|
179
221
|
|
|
180
222
|
The ign8t MCP server is designed to fit seamlessly into your development workflow:
|
|
181
223
|
|
|
182
224
|
1. **Start your day**: "What are my current tasks?"
|
|
183
|
-
2. **
|
|
184
|
-
3. **
|
|
185
|
-
4. **
|
|
186
|
-
5. **
|
|
225
|
+
2. **Find the next build unit**: "What task should I work on next?"
|
|
226
|
+
3. **Check spec coverage**: "What specs are left?"
|
|
227
|
+
4. **Get context**: "Show me the task context and master spec"
|
|
228
|
+
5. **Add discovered work**: "Queue a recommendation for webhook retry support"
|
|
229
|
+
6. **Accept planned work**: "Accept that recommendation and create a task"
|
|
230
|
+
7. **Update progress**: "Mark this task as in progress"
|
|
231
|
+
8. **Complete work**: "Mark task as done and get my next task"
|
|
187
232
|
|
|
188
233
|
## Troubleshooting
|
|
189
234
|
|
|
@@ -218,22 +263,20 @@ The ign8t MCP server is designed to fit seamlessly into your development workflo
|
|
|
218
263
|
|
|
219
264
|
### Custom API Endpoint (Optional)
|
|
220
265
|
|
|
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):**
|
|
266
|
+
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
267
|
|
|
225
268
|
```json
|
|
226
269
|
{
|
|
227
270
|
"mcpServers": {
|
|
228
271
|
"ign8t": {
|
|
229
272
|
"command": "npx",
|
|
230
|
-
"args": ["-y", "@ign8t/mcp@latest", "--api-key", "your-api-key"]
|
|
273
|
+
"args": ["-y", "@ign8t/mcp@latest", "--api-key", "your-api-key", "--api-url", "http://localhost:3000"]
|
|
231
274
|
}
|
|
232
275
|
}
|
|
233
276
|
}
|
|
234
277
|
```
|
|
235
278
|
|
|
236
|
-
|
|
279
|
+
Or set `IGN8T_API_URL` in the `env` block of the same IDE configuration. A `.env` file in your workspace is not read.
|
|
237
280
|
|
|
238
281
|
**Note**: Most users don't need to set a custom API URL. It's only required if you're:
|
|
239
282
|
|
|
@@ -250,32 +293,8 @@ For contributors working on the MCP server itself:
|
|
|
250
293
|
3. Build the project: `pnpm build`
|
|
251
294
|
4. Test locally: `pnpm dev --api-key your-key`
|
|
252
295
|
|
|
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
296
|
## Support
|
|
275
297
|
|
|
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
298
|
- 📧 **Email**: support@ign8t.com
|
|
280
299
|
|
|
281
300
|
## 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.2.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,29 +68,30 @@ 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
96
|
// Define available tools
|
|
97
97
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -120,6 +120,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
120
120
|
properties: {
|
|
121
121
|
project_id: {
|
|
122
122
|
type: "string",
|
|
123
|
+
format: "uuid",
|
|
123
124
|
description: "The project ID",
|
|
124
125
|
},
|
|
125
126
|
},
|
|
@@ -128,17 +129,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
128
129
|
},
|
|
129
130
|
{
|
|
130
131
|
name: "get_documents",
|
|
131
|
-
description: "Get
|
|
132
|
+
description: "Get master specs, task specs, and legacy BRD/PRD/TRD documents for a project",
|
|
132
133
|
inputSchema: {
|
|
133
134
|
type: "object",
|
|
134
135
|
properties: {
|
|
135
136
|
project_id: {
|
|
136
137
|
type: "string",
|
|
138
|
+
format: "uuid",
|
|
137
139
|
description: "The project ID",
|
|
138
140
|
},
|
|
139
141
|
document_type: {
|
|
140
142
|
type: "string",
|
|
141
|
-
enum: ["brd", "prd", "trd", "all"],
|
|
143
|
+
enum: ["master_spec", "prp", "brd", "prd", "trd", "task_spec", "all"],
|
|
142
144
|
description: "Type of document to retrieve",
|
|
143
145
|
default: "all",
|
|
144
146
|
},
|
|
@@ -154,6 +156,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
154
156
|
properties: {
|
|
155
157
|
project_id: {
|
|
156
158
|
type: "string",
|
|
159
|
+
format: "uuid",
|
|
157
160
|
description: "The project ID",
|
|
158
161
|
},
|
|
159
162
|
include_completed: {
|
|
@@ -166,18 +169,154 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
166
169
|
},
|
|
167
170
|
},
|
|
168
171
|
{
|
|
169
|
-
name: "
|
|
170
|
-
description: "
|
|
172
|
+
name: "get_specs",
|
|
173
|
+
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.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
project_id: {
|
|
178
|
+
type: "string",
|
|
179
|
+
format: "uuid",
|
|
180
|
+
description: "The project ID",
|
|
181
|
+
},
|
|
182
|
+
include_generated: {
|
|
183
|
+
type: "boolean",
|
|
184
|
+
description: "Include specs that already exist",
|
|
185
|
+
default: true,
|
|
186
|
+
},
|
|
187
|
+
include_missing: {
|
|
188
|
+
type: "boolean",
|
|
189
|
+
description: "Include backlog tasks without generated specs",
|
|
190
|
+
default: true,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
required: ["project_id"],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "create_task",
|
|
198
|
+
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.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
project_id: {
|
|
203
|
+
type: "string",
|
|
204
|
+
format: "uuid",
|
|
205
|
+
description: "The project ID",
|
|
206
|
+
},
|
|
207
|
+
title: {
|
|
208
|
+
type: "string",
|
|
209
|
+
minLength: 3,
|
|
210
|
+
maxLength: 200,
|
|
211
|
+
description: "Short task title",
|
|
212
|
+
},
|
|
213
|
+
description: {
|
|
214
|
+
type: "string",
|
|
215
|
+
maxLength: 10000,
|
|
216
|
+
description: "Detailed task description",
|
|
217
|
+
},
|
|
218
|
+
priority: {
|
|
219
|
+
type: "string",
|
|
220
|
+
enum: ["critical", "high", "medium", "low"],
|
|
221
|
+
description: "Task priority",
|
|
222
|
+
default: "medium",
|
|
223
|
+
},
|
|
224
|
+
status: {
|
|
225
|
+
type: "string",
|
|
226
|
+
enum: ["todo", "in_progress", "review", "done", "blocked"],
|
|
227
|
+
description: "Initial task status",
|
|
228
|
+
default: "todo",
|
|
229
|
+
},
|
|
230
|
+
acceptance_criteria: {
|
|
231
|
+
type: "array",
|
|
232
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
233
|
+
maxItems: 30,
|
|
234
|
+
description: "Acceptance criteria for the task",
|
|
235
|
+
},
|
|
236
|
+
technical_details: {
|
|
237
|
+
type: "string",
|
|
238
|
+
maxLength: 10000,
|
|
239
|
+
description: "Implementation notes or technical requirements",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ["project_id", "title"],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "list_recommendations",
|
|
247
|
+
description: "List backlog recommendations queued for review. Use this after adding feature requests or when deciding what to build next.",
|
|
171
248
|
inputSchema: {
|
|
172
249
|
type: "object",
|
|
173
250
|
properties: {
|
|
174
251
|
project_id: {
|
|
175
252
|
type: "string",
|
|
253
|
+
format: "uuid",
|
|
176
254
|
description: "The project ID",
|
|
177
255
|
},
|
|
178
|
-
|
|
256
|
+
status: {
|
|
179
257
|
type: "string",
|
|
180
|
-
|
|
258
|
+
enum: ["pending", "accepted", "rejected", "expired", "all"],
|
|
259
|
+
description: "Recommendation status to list",
|
|
260
|
+
default: "pending",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ["project_id"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "accept_recommendations",
|
|
268
|
+
description: "Accept pending backlog recommendations and create concrete tasks from them.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
project_id: {
|
|
273
|
+
type: "string",
|
|
274
|
+
format: "uuid",
|
|
275
|
+
description: "The project ID",
|
|
276
|
+
},
|
|
277
|
+
recommendation_ids: {
|
|
278
|
+
type: "array",
|
|
279
|
+
items: { type: "string", format: "uuid" },
|
|
280
|
+
minItems: 1,
|
|
281
|
+
maxItems: 50,
|
|
282
|
+
description: "Recommendation IDs to accept",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
required: ["project_id", "recommendation_ids"],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "reject_recommendations",
|
|
290
|
+
description: "Reject pending backlog recommendations so they no longer appear as work candidates.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
project_id: {
|
|
295
|
+
type: "string",
|
|
296
|
+
format: "uuid",
|
|
297
|
+
description: "The project ID",
|
|
298
|
+
},
|
|
299
|
+
recommendation_ids: {
|
|
300
|
+
type: "array",
|
|
301
|
+
items: { type: "string", format: "uuid" },
|
|
302
|
+
minItems: 1,
|
|
303
|
+
maxItems: 50,
|
|
304
|
+
description: "Recommendation IDs to reject",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
required: ["project_id", "recommendation_ids"],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "get_next_task",
|
|
312
|
+
description: "Get the next todo task to work on (highest priority, then oldest), including its generated task spec when available.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
project_id: {
|
|
317
|
+
type: "string",
|
|
318
|
+
format: "uuid",
|
|
319
|
+
description: "The project ID",
|
|
181
320
|
},
|
|
182
321
|
},
|
|
183
322
|
required: ["project_id"],
|
|
@@ -191,6 +330,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
191
330
|
properties: {
|
|
192
331
|
task_id: {
|
|
193
332
|
type: "string",
|
|
333
|
+
format: "uuid",
|
|
194
334
|
description: "The task ID",
|
|
195
335
|
},
|
|
196
336
|
status: {
|
|
@@ -200,6 +340,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
200
340
|
},
|
|
201
341
|
notes: {
|
|
202
342
|
type: "string",
|
|
343
|
+
maxLength: 10000,
|
|
203
344
|
description: "Optional notes about the status change",
|
|
204
345
|
},
|
|
205
346
|
},
|
|
@@ -218,6 +359,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
218
359
|
},
|
|
219
360
|
project_id: {
|
|
220
361
|
type: "string",
|
|
362
|
+
format: "uuid",
|
|
221
363
|
description: "Optional: limit search to specific project",
|
|
222
364
|
},
|
|
223
365
|
status: {
|
|
@@ -232,23 +374,214 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
232
374
|
},
|
|
233
375
|
{
|
|
234
376
|
name: "get_task_context",
|
|
235
|
-
description: "Get
|
|
377
|
+
description: "Get a task with its story, epic, its own task spec and the project's master spec",
|
|
236
378
|
inputSchema: {
|
|
237
379
|
type: "object",
|
|
238
380
|
properties: {
|
|
239
381
|
task_id: {
|
|
240
382
|
type: "string",
|
|
383
|
+
format: "uuid",
|
|
241
384
|
description: "The task ID",
|
|
242
385
|
},
|
|
243
386
|
},
|
|
244
387
|
required: ["task_id"],
|
|
245
388
|
},
|
|
246
389
|
},
|
|
390
|
+
{
|
|
391
|
+
name: "import_project",
|
|
392
|
+
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.",
|
|
393
|
+
inputSchema: {
|
|
394
|
+
type: "object",
|
|
395
|
+
properties: {
|
|
396
|
+
project_id: {
|
|
397
|
+
type: "string",
|
|
398
|
+
format: "uuid",
|
|
399
|
+
description: "The ign8t project ID to import into",
|
|
400
|
+
},
|
|
401
|
+
workspace_path: {
|
|
402
|
+
type: "string",
|
|
403
|
+
description: "Absolute path to the project root directory",
|
|
404
|
+
},
|
|
405
|
+
locale: {
|
|
406
|
+
type: "string",
|
|
407
|
+
enum: ["ko", "en"],
|
|
408
|
+
description: "Language of the extracted context",
|
|
409
|
+
default: "ko",
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
required: ["project_id", "workspace_path"],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
// ===== BIDIRECTIONAL CONTEXT UPDATE TOOLS =====
|
|
416
|
+
{
|
|
417
|
+
name: "add_feature_request",
|
|
418
|
+
description: "Request a new feature spec candidate discovered during development. This queues the idea for product-owner review before implementation.",
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
project_id: {
|
|
423
|
+
type: "string",
|
|
424
|
+
format: "uuid",
|
|
425
|
+
description: "The project ID",
|
|
426
|
+
},
|
|
427
|
+
feature: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
name: {
|
|
431
|
+
type: "string",
|
|
432
|
+
minLength: 1,
|
|
433
|
+
maxLength: 200,
|
|
434
|
+
description: "Feature name (e.g., 'OAuth Social Login')",
|
|
435
|
+
},
|
|
436
|
+
description: {
|
|
437
|
+
type: "string",
|
|
438
|
+
minLength: 1,
|
|
439
|
+
maxLength: 5000,
|
|
440
|
+
description: "Detailed feature description",
|
|
441
|
+
},
|
|
442
|
+
user_story: {
|
|
443
|
+
type: "string",
|
|
444
|
+
minLength: 1,
|
|
445
|
+
maxLength: 2000,
|
|
446
|
+
description: "User story (As a... I want... So that...)",
|
|
447
|
+
},
|
|
448
|
+
priority: {
|
|
449
|
+
type: "string",
|
|
450
|
+
enum: ["critical", "high", "medium", "low"],
|
|
451
|
+
description: "Feature priority",
|
|
452
|
+
},
|
|
453
|
+
estimated_complexity: {
|
|
454
|
+
type: "string",
|
|
455
|
+
enum: ["simple", "moderate", "complex"],
|
|
456
|
+
description: "Estimated implementation complexity",
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
required: ["name", "description", "user_story"],
|
|
460
|
+
},
|
|
461
|
+
related_context: {
|
|
462
|
+
type: "object",
|
|
463
|
+
description: "Additional context for spec definition (optional, at most 10000 characters as JSON)",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
required: ["project_id", "feature"],
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "add_user_persona",
|
|
471
|
+
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.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: "object",
|
|
474
|
+
properties: {
|
|
475
|
+
project_id: {
|
|
476
|
+
type: "string",
|
|
477
|
+
format: "uuid",
|
|
478
|
+
description: "The project ID",
|
|
479
|
+
},
|
|
480
|
+
persona: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
name: {
|
|
484
|
+
type: "string",
|
|
485
|
+
minLength: 1,
|
|
486
|
+
maxLength: 100,
|
|
487
|
+
description: "Persona name (e.g., 'Sarah - Operations Manager')",
|
|
488
|
+
},
|
|
489
|
+
role: {
|
|
490
|
+
type: "string",
|
|
491
|
+
minLength: 1,
|
|
492
|
+
maxLength: 200,
|
|
493
|
+
description: "User role or job title",
|
|
494
|
+
},
|
|
495
|
+
demographics: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
age: { type: "string", maxLength: 100 },
|
|
499
|
+
tech_savviness: {
|
|
500
|
+
type: "string",
|
|
501
|
+
enum: ["beginner", "intermediate", "advanced"],
|
|
502
|
+
},
|
|
503
|
+
industry: { type: "string", maxLength: 100 },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
goals: {
|
|
507
|
+
type: "array",
|
|
508
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
509
|
+
maxItems: 20,
|
|
510
|
+
description: "User goals and objectives",
|
|
511
|
+
},
|
|
512
|
+
pain_points: {
|
|
513
|
+
type: "array",
|
|
514
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
515
|
+
maxItems: 20,
|
|
516
|
+
description: "User pain points and frustrations",
|
|
517
|
+
},
|
|
518
|
+
use_cases: {
|
|
519
|
+
type: "array",
|
|
520
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
521
|
+
maxItems: 20,
|
|
522
|
+
description: "Specific use cases for this persona",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
required: ["name", "role", "goals", "pain_points"],
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
required: ["project_id", "persona"],
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: "update_success_criteria",
|
|
533
|
+
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.",
|
|
534
|
+
inputSchema: {
|
|
535
|
+
type: "object",
|
|
536
|
+
properties: {
|
|
537
|
+
project_id: {
|
|
538
|
+
type: "string",
|
|
539
|
+
format: "uuid",
|
|
540
|
+
description: "The project ID",
|
|
541
|
+
},
|
|
542
|
+
criteria: {
|
|
543
|
+
type: "array",
|
|
544
|
+
minItems: 1,
|
|
545
|
+
maxItems: 20,
|
|
546
|
+
items: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {
|
|
549
|
+
metric: {
|
|
550
|
+
type: "string",
|
|
551
|
+
minLength: 1,
|
|
552
|
+
maxLength: 200,
|
|
553
|
+
pattern: "^[^:]*$",
|
|
554
|
+
description: "What to measure, without ':' (e.g., 'User sign-up rate')",
|
|
555
|
+
},
|
|
556
|
+
target: {
|
|
557
|
+
type: "string",
|
|
558
|
+
minLength: 1,
|
|
559
|
+
maxLength: 200,
|
|
560
|
+
description: "Target value (e.g., '80% of visitors')",
|
|
561
|
+
},
|
|
562
|
+
priority: {
|
|
563
|
+
type: "string",
|
|
564
|
+
enum: ["must-have", "nice-to-have"],
|
|
565
|
+
description: "Metric priority",
|
|
566
|
+
},
|
|
567
|
+
measurement_method: {
|
|
568
|
+
type: "string",
|
|
569
|
+
maxLength: 500,
|
|
570
|
+
description: "How this will be measured (optional)",
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
required: ["metric", "target", "priority"],
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
required: ["project_id", "criteria"],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
247
580
|
],
|
|
248
581
|
};
|
|
249
582
|
});
|
|
250
583
|
// Handle tool calls
|
|
251
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
584
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
252
585
|
const { name, arguments: args } = request.params;
|
|
253
586
|
try {
|
|
254
587
|
let result;
|
|
@@ -265,6 +598,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
265
598
|
case "get_backlog":
|
|
266
599
|
result = await callIgn8tAPI("/backlog/get", args);
|
|
267
600
|
break;
|
|
601
|
+
case "get_specs":
|
|
602
|
+
result = await callIgn8tAPI("/specs/list", args);
|
|
603
|
+
break;
|
|
604
|
+
case "create_task":
|
|
605
|
+
result = await callIgn8tAPI("/tasks/create", args);
|
|
606
|
+
break;
|
|
607
|
+
case "list_recommendations":
|
|
608
|
+
result = await callIgn8tAPI("/recommendations/list", args);
|
|
609
|
+
break;
|
|
610
|
+
case "accept_recommendations":
|
|
611
|
+
result = await callIgn8tAPI("/recommendations/accept", args);
|
|
612
|
+
break;
|
|
613
|
+
case "reject_recommendations":
|
|
614
|
+
result = await callIgn8tAPI("/recommendations/reject", args);
|
|
615
|
+
break;
|
|
268
616
|
case "get_next_task":
|
|
269
617
|
result = await callIgn8tAPI("/tasks/next", args);
|
|
270
618
|
break;
|
|
@@ -277,6 +625,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
277
625
|
case "get_task_context":
|
|
278
626
|
result = await callIgn8tAPI("/tasks/context", args);
|
|
279
627
|
break;
|
|
628
|
+
case "import_project": {
|
|
629
|
+
const { snapshot, unreadableDirectories, fileLimitReached } = await buildWorkspaceSnapshot(args?.workspace_path);
|
|
630
|
+
// The upload is what charges Sparks: a call the user cancelled while the folder
|
|
631
|
+
// was being read must not go on to charge.
|
|
632
|
+
extra.signal.throwIfAborted();
|
|
633
|
+
// The server's reply states what happened: sparks_charged, already_imported, warning.
|
|
634
|
+
result = await callIgn8tAPI("/projects/import", {
|
|
635
|
+
project_id: args?.project_id,
|
|
636
|
+
locale: args?.locale,
|
|
637
|
+
workspace: snapshot,
|
|
638
|
+
});
|
|
639
|
+
if (unreadableDirectories.length) {
|
|
640
|
+
result = { ...result, skipped_unreadable_directories: unreadableDirectories };
|
|
641
|
+
}
|
|
642
|
+
if (fileLimitReached) {
|
|
643
|
+
result = {
|
|
644
|
+
...result,
|
|
645
|
+
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).",
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
// ===== BIDIRECTIONAL CONTEXT UPDATE HANDLERS =====
|
|
651
|
+
case "add_feature_request": {
|
|
652
|
+
console.error(`Adding feature request for project: ${args.project_id}`);
|
|
653
|
+
result = await callIgn8tAPI("/features/add", {
|
|
654
|
+
project_id: args.project_id,
|
|
655
|
+
feature: args.feature,
|
|
656
|
+
related_context: args.related_context,
|
|
657
|
+
mcp_metadata: {
|
|
658
|
+
ide: "MCP Client",
|
|
659
|
+
timestamp: new Date().toISOString(),
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
console.error("Feature request submitted successfully");
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "add_user_persona": {
|
|
666
|
+
console.error(`Adding user persona for project: ${args.project_id}`);
|
|
667
|
+
result = await callIgn8tAPI("/personas/add", {
|
|
668
|
+
project_id: args.project_id,
|
|
669
|
+
persona: args.persona,
|
|
670
|
+
mcp_metadata: {
|
|
671
|
+
ide: "MCP Client",
|
|
672
|
+
timestamp: new Date().toISOString(),
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
console.error("User persona added successfully");
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
case "update_success_criteria": {
|
|
679
|
+
console.error(`Updating success criteria for project: ${args.project_id}`);
|
|
680
|
+
result = await callIgn8tAPI("/criteria/update", {
|
|
681
|
+
project_id: args.project_id,
|
|
682
|
+
criteria: args.criteria,
|
|
683
|
+
mcp_metadata: {
|
|
684
|
+
ide: "MCP Client",
|
|
685
|
+
timestamp: new Date().toISOString(),
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
console.error("Success criteria updated successfully");
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
280
691
|
default:
|
|
281
692
|
throw new Error(`Unknown tool: ${name}`);
|
|
282
693
|
}
|
|
@@ -368,7 +779,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
368
779
|
async function main() {
|
|
369
780
|
const transport = new StdioServerTransport();
|
|
370
781
|
await server.connect(transport);
|
|
371
|
-
console.error(
|
|
782
|
+
console.error(`ign8t MCP server v${VERSION}`);
|
|
372
783
|
console.error(`Connected to: ${API_URL}`);
|
|
373
784
|
console.error("Ready to serve requests...");
|
|
374
785
|
}
|
|
@@ -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.2.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.2.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
|
+
}
|