@ign8t/mcp 0.1.1 → 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 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 BRD, PRD, and TRD documents to understand requirements
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
- - 🎯 Get AI-powered task recommendations
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/settings.json`:
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 BRD for [project name]"
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 | Description | Example Use |
168
- | -------------------- | --------------------------------------------- | ------------------------------------------- |
169
- | `list_projects` | List all projects with optional status filter | "Show my archived projects" |
170
- | `get_project` | Get detailed project information | "Get details for project abc123" |
171
- | `get_documents` | Retrieve BRD, PRD, TRD documents | "Show me the PRD for project xyz" |
172
- | `get_backlog` | View epics, stories, and tasks | "Display backlog including completed items" |
173
- | `get_next_task` | AI-recommended next task | "What should I work on next?" |
174
- | `update_task_status` | Change task status with notes | "Set task 123 to blocked: waiting on API" |
175
- | `search_tasks` | Search across tasks | "Find all tasks mentioning 'payment'" |
176
- | `get_task_context` | Full task context with requirements | "Show context for task 456" |
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. **Get context**: "Show me the requirements for this task"
184
- 3. **Update progress**: "Mark this task as in progress"
185
- 4. **Search when stuck**: "Find similar tasks I've completed"
186
- 5. **Complete work**: "Mark task as done and get my next task"
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 * as dotenv from "dotenv";
6
- // Load environment variables
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,48 +40,58 @@ 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: "1.0.0",
55
+ version: VERSION,
56
+ description: "ign8t project management integration for AI IDEs",
57
+ author: "nocodecat",
58
+ homepage: "https://www.ign8t.com",
57
59
  }, {
58
60
  capabilities: {
59
- resources: {},
60
- tools: {},
61
+ resources: {
62
+ subscribe: false,
63
+ listChanged: false,
64
+ },
65
+ tools: {
66
+ listChanged: false,
67
+ },
68
+ logging: {},
61
69
  },
62
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
+ }
63
81
  // Helper function to call ign8t API
64
82
  async function callIgn8tAPI(endpoint, data = {}) {
65
- try {
66
- const response = await fetch(`${API_URL}/api/mcp${endpoint}`, {
67
- method: "POST",
68
- headers: {
69
- "Content-Type": "application/json",
70
- "X-API-Key": API_KEY,
71
- },
72
- body: JSON.stringify(data),
73
- });
74
- if (!response.ok) {
75
- const error = await response.text();
76
- throw new Error(error || `API error: ${response.status}`);
77
- }
78
- return response.json();
79
- }
80
- catch (error) {
81
- if (error instanceof Error) {
82
- throw error;
83
- }
84
- 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));
85
93
  }
94
+ return response.json();
86
95
  }
87
96
  // Define available tools
88
97
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -111,6 +120,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
111
120
  properties: {
112
121
  project_id: {
113
122
  type: "string",
123
+ format: "uuid",
114
124
  description: "The project ID",
115
125
  },
116
126
  },
@@ -119,17 +129,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
119
129
  },
120
130
  {
121
131
  name: "get_documents",
122
- description: "Get BRD, PRD, and TRD documents for a project",
132
+ description: "Get master specs, task specs, and legacy BRD/PRD/TRD documents for a project",
123
133
  inputSchema: {
124
134
  type: "object",
125
135
  properties: {
126
136
  project_id: {
127
137
  type: "string",
138
+ format: "uuid",
128
139
  description: "The project ID",
129
140
  },
130
141
  document_type: {
131
142
  type: "string",
132
- enum: ["brd", "prd", "trd", "all"],
143
+ enum: ["master_spec", "prp", "brd", "prd", "trd", "task_spec", "all"],
133
144
  description: "Type of document to retrieve",
134
145
  default: "all",
135
146
  },
@@ -145,6 +156,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
145
156
  properties: {
146
157
  project_id: {
147
158
  type: "string",
159
+ format: "uuid",
148
160
  description: "The project ID",
149
161
  },
150
162
  include_completed: {
@@ -157,18 +169,154 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
157
169
  },
158
170
  },
159
171
  {
160
- name: "get_next_task",
161
- description: "Get the next recommended task to work on",
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.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ project_id: {
252
+ type: "string",
253
+ format: "uuid",
254
+ description: "The project ID",
255
+ },
256
+ status: {
257
+ type: "string",
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.",
162
291
  inputSchema: {
163
292
  type: "object",
164
293
  properties: {
165
294
  project_id: {
166
295
  type: "string",
296
+ format: "uuid",
167
297
  description: "The project ID",
168
298
  },
169
- context: {
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: {
170
317
  type: "string",
171
- description: "Optional context about what you want to work on",
318
+ format: "uuid",
319
+ description: "The project ID",
172
320
  },
173
321
  },
174
322
  required: ["project_id"],
@@ -182,6 +330,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
182
330
  properties: {
183
331
  task_id: {
184
332
  type: "string",
333
+ format: "uuid",
185
334
  description: "The task ID",
186
335
  },
187
336
  status: {
@@ -191,6 +340,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
191
340
  },
192
341
  notes: {
193
342
  type: "string",
343
+ maxLength: 10000,
194
344
  description: "Optional notes about the status change",
195
345
  },
196
346
  },
@@ -209,6 +359,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
209
359
  },
210
360
  project_id: {
211
361
  type: "string",
362
+ format: "uuid",
212
363
  description: "Optional: limit search to specific project",
213
364
  },
214
365
  status: {
@@ -223,23 +374,214 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
223
374
  },
224
375
  {
225
376
  name: "get_task_context",
226
- description: "Get full context for a task including related BRD/PRD/TRD sections",
377
+ description: "Get a task with its story, epic, its own task spec and the project's master spec",
227
378
  inputSchema: {
228
379
  type: "object",
229
380
  properties: {
230
381
  task_id: {
231
382
  type: "string",
383
+ format: "uuid",
232
384
  description: "The task ID",
233
385
  },
234
386
  },
235
387
  required: ["task_id"],
236
388
  },
237
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
+ },
238
580
  ],
239
581
  };
240
582
  });
241
583
  // Handle tool calls
242
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
584
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
243
585
  const { name, arguments: args } = request.params;
244
586
  try {
245
587
  let result;
@@ -256,6 +598,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
598
  case "get_backlog":
257
599
  result = await callIgn8tAPI("/backlog/get", args);
258
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;
259
616
  case "get_next_task":
260
617
  result = await callIgn8tAPI("/tasks/next", args);
261
618
  break;
@@ -268,6 +625,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
268
625
  case "get_task_context":
269
626
  result = await callIgn8tAPI("/tasks/context", args);
270
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
+ }
271
691
  default:
272
692
  throw new Error(`Unknown tool: ${name}`);
273
693
  }
@@ -359,7 +779,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
359
779
  async function main() {
360
780
  const transport = new StdioServerTransport();
361
781
  await server.connect(transport);
362
- console.error("ign8t MCP server v1.0.0");
782
+ console.error(`ign8t MCP server v${VERSION}`);
363
783
  console.error(`Connected to: ${API_URL}`);
364
784
  console.error("Ready to serve requests...");
365
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.1.1",
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",
@@ -12,7 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc",
15
+ "build": "tsc && chmod +x dist/index.js",
16
16
  "build:watch": "tsc --watch",
17
17
  "start": "node dist/index.js",
18
18
  "dev": "tsx src/index.ts",
@@ -26,22 +26,25 @@
26
26
  },
27
27
  "keywords": [
28
28
  "mcp",
29
+ "model-context-protocol",
30
+ "mcp-server",
29
31
  "ign8t",
30
32
  "ai",
31
33
  "ai-ide",
32
34
  "cursor",
33
- "claude"
35
+ "claude",
36
+ "project-management"
34
37
  ],
38
+ "mcp": {
39
+ "name": "ign8t-mcp",
40
+ "description": "MCP server for ign8t project management - AI IDE integration",
41
+ "version": "0.2.0"
42
+ },
35
43
  "author": "nocodecat",
36
44
  "license": "MIT",
37
- "homepage": "https://github.com/nocodecat/mcp-server",
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/nocodecat/mcp-server.git"
41
- },
45
+ "homepage": "https://www.ign8t.com",
42
46
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^0.5.0",
44
- "dotenv": "^16.3.1"
47
+ "@modelcontextprotocol/sdk": "^0.5.0"
45
48
  },
46
49
  "devDependencies": {
47
50
  "@types/node": "^20.10.5",