@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 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,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: "1.0.0",
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
- try {
75
- const response = await fetch(`${API_URL}/api/mcp${endpoint}`, {
76
- method: "POST",
77
- headers: {
78
- "Content-Type": "application/json",
79
- "X-API-Key": API_KEY,
80
- },
81
- body: JSON.stringify(data),
82
- });
83
- if (!response.ok) {
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 BRD, PRD, and TRD documents for a project",
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: "get_next_task",
170
- 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.",
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
- context: {
256
+ status: {
179
257
  type: "string",
180
- description: "Optional context about what you want to work on",
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 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",
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("ign8t MCP server v1.0.0");
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.1.2",
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": "1.0.0"
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
+ }