@igor-olikh/openspec-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/dist/index.js +22 -0
- package/dist/server.js +73 -0
- package/dist/tools.js +197 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# OpenSpec MCP Server (AI Assistant Plugin)
|
|
2
|
+
|
|
3
|
+
Welcome! This is a simple bridge (plugin) that connects **[OpenSpec](https://github.com/Fission-AI/OpenSpec)** to your favorite AI coding assistant (like **Codex**, **Claude Desktop**, or **Cursor**).
|
|
4
|
+
|
|
5
|
+
## What is this and why do I need it?
|
|
6
|
+
When you want your AI to build a new feature, you usually just type it into the chat. But as projects grow, the AI can forget things, get confused, or write messy code.
|
|
7
|
+
|
|
8
|
+
**OpenSpec** is a system that solves this. It forces the AI to create a clear "specification" (a plan) before it writes any code. It organizes your plan into neat folders (`proposal`, `design`, `tasks`) so you can review it.
|
|
9
|
+
|
|
10
|
+
However, your AI doesn't automatically know how to use OpenSpec. **That is what this server does!** It gives your AI the "tools" it needs to automatically create these folders, list tasks, and mark them as complete as it writes code for you.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## How to Connect Your AI
|
|
15
|
+
|
|
16
|
+
To use this, you need to tell your AI assistant where this server is located. The setup simply depends on which AI assistant you use.
|
|
17
|
+
|
|
18
|
+
### Option 1: Connecting to Codex (Recommended)
|
|
19
|
+
|
|
20
|
+
Codex has a built-in user interface to easily add these plugins.
|
|
21
|
+
You can add it quickly by running this terminal command:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
codex mcp add openspec-server node /Users/igorolikh/Documents/projects/private/openspec-mcp-server/dist/index.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Or, manually through the Codex User Interface:**
|
|
28
|
+
1. Open the **"Connect to a custom MCP"** box in Codex.
|
|
29
|
+
2. **Name**: `openspec`
|
|
30
|
+
3. **Mode**: Leave as `STDIO`
|
|
31
|
+
4. **Command to launch**: `node`
|
|
32
|
+
5. **Arguments**: Click `+ Add argument` and paste exactly:
|
|
33
|
+
`/Users/igorolikh/Documents/projects/private/openspec-mcp-server/dist/index.js`
|
|
34
|
+
6. **Working directory**: Leave this blank! (This allows Codex to dynamically use OpenSpec inside whichever project you currently have open).
|
|
35
|
+
7. Save it!
|
|
36
|
+
|
|
37
|
+
### Option 2: Connecting to Claude Desktop App
|
|
38
|
+
|
|
39
|
+
If you prefer using the Claude Desktop application:
|
|
40
|
+
1. Open your Claude configuration file (usually located at `~/Library/Application Support/Claude/claude_desktop_config.json` on Mac).
|
|
41
|
+
2. Add the `openspec` server to it:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"openspec": {
|
|
47
|
+
"command": "node",
|
|
48
|
+
"args": [
|
|
49
|
+
"/Users/igorolikh/Documents/projects/private/openspec-mcp-server/dist/index.js"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
3. Save the file and restart Claude Desktop.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## How do I use it?
|
|
60
|
+
|
|
61
|
+
Once connected, you don't need to do anything technical. You just talk to your AI like normal, but ask it to use OpenSpec!
|
|
62
|
+
|
|
63
|
+
**Example Chat Prompts:**
|
|
64
|
+
* *"Hey Codex, I want to add a dark mode feature to this application. Please use OpenSpec to propose and validate it."*
|
|
65
|
+
* *"What is the OpenSpec status of our current project?"*
|
|
66
|
+
* *"List all the OpenSpec changes we are currently working on."*
|
|
67
|
+
|
|
68
|
+
The AI will automatically use the tools below to handle the rest!
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## For Developers (Under the Hood)
|
|
73
|
+
|
|
74
|
+
This server exposes the official `@fission-ai/openspec` CLI commands as Model Context Protocol (MCP) JSON-RPC tools.
|
|
75
|
+
|
|
76
|
+
**Available AI Tools:**
|
|
77
|
+
- `openspec_init`: Starts OpenSpec in a project.
|
|
78
|
+
- `openspec_new_change`: Creates a folder for a new feature proposal.
|
|
79
|
+
- `openspec_status`: Checks how much of the feature is done.
|
|
80
|
+
- `openspec_validate`: Checks if the code matches the plan.
|
|
81
|
+
- `openspec_archive`: Marks the feature as 100% completed.
|
|
82
|
+
- `openspec_list`: Shows all current tasks.
|
|
83
|
+
- `openspec_show`: Reads a specific task.
|
|
84
|
+
- `openspec_update`: Updates OpenSpec rules.
|
|
85
|
+
- `openspec_instructions`: Reads AI instructions for building parts of the plan.
|
|
86
|
+
|
|
87
|
+
### Development Setup
|
|
88
|
+
If you want to modify this server's code:
|
|
89
|
+
1. `npm install` (Installs dependencies)
|
|
90
|
+
2. `npm run build` (Compiles the code)
|
|
91
|
+
3. `npm run start` (Runs the server to test standard input/output)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { OpenSpecMCPServer } from './server.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
async function main() {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const projectPath = args[0] || process.cwd();
|
|
7
|
+
const resolvedPath = path.resolve(projectPath);
|
|
8
|
+
console.error(`Starting OpenSpec MCP Server for project: ${resolvedPath}`);
|
|
9
|
+
const server = new OpenSpecMCPServer();
|
|
10
|
+
await server.initialize(resolvedPath);
|
|
11
|
+
// Handle graceful shutdown
|
|
12
|
+
const shutdown = async () => {
|
|
13
|
+
await server.stop();
|
|
14
|
+
process.exit(0);
|
|
15
|
+
};
|
|
16
|
+
process.on('SIGINT', shutdown);
|
|
17
|
+
process.on('SIGTERM', shutdown);
|
|
18
|
+
}
|
|
19
|
+
main().catch((error) => {
|
|
20
|
+
console.error('Fatal error:', error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { getTools, handleToolCall } from './tools.js';
|
|
5
|
+
export class OpenSpecMCPServer {
|
|
6
|
+
server;
|
|
7
|
+
projectPath = process.cwd();
|
|
8
|
+
constructor() {
|
|
9
|
+
this.server = new Server({
|
|
10
|
+
name: 'openspec-mcp-server',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
}, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
this.setupHandlers();
|
|
18
|
+
}
|
|
19
|
+
setupHandlers() {
|
|
20
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21
|
+
// Provide dynamic tools or static ones
|
|
22
|
+
return {
|
|
23
|
+
tools: getTools(),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await handleToolCall(request.params.name, request.params.arguments || {}, this.projectPath);
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: result.message || 'Tool executed successfully',
|
|
34
|
+
},
|
|
35
|
+
...(result.stdout ? [{ type: 'text', text: `Output:\n${result.stdout}` }] : []),
|
|
36
|
+
...(result.stderr ? [{ type: 'text', text: `Error Output:\n${result.stderr}` }] : [])
|
|
37
|
+
],
|
|
38
|
+
isError: !result.success
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new McpError(ErrorCode.InternalError, error.message);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async initialize(projectPath) {
|
|
47
|
+
this.projectPath = projectPath;
|
|
48
|
+
// Connect to stdio transport
|
|
49
|
+
const transport = new StdioServerTransport();
|
|
50
|
+
transport.onclose = async () => {
|
|
51
|
+
await this.stop();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
};
|
|
54
|
+
await this.server.connect(transport);
|
|
55
|
+
process.stdin.on('end', async () => {
|
|
56
|
+
await this.stop();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
59
|
+
process.stdin.on('error', async (error) => {
|
|
60
|
+
console.error('stdin error:', error);
|
|
61
|
+
await this.stop();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async stop() {
|
|
66
|
+
try {
|
|
67
|
+
await this.server.close();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error('Error closing server:', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
const OPENSPEC_CMD = 'npx --yes @fission-ai/openspec';
|
|
5
|
+
export function getTools() {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
name: 'openspec_init',
|
|
9
|
+
description: 'Initialize OpenSpec in your project',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {},
|
|
13
|
+
required: []
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'openspec_update',
|
|
18
|
+
description: 'Update OpenSpec instruction files',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {},
|
|
22
|
+
required: []
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'openspec_list',
|
|
27
|
+
description: 'List items (changes or specs). Returns a JSON array when --json is used.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
specs: { type: 'boolean', description: 'List specs instead of changes' },
|
|
32
|
+
json: { type: 'boolean', description: 'Output as JSON', default: true }
|
|
33
|
+
},
|
|
34
|
+
required: []
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'openspec_show',
|
|
39
|
+
description: 'Show a change or spec. Use type to specify if it is ambiguous.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
itemName: { type: 'string', description: 'Name of the item to show' },
|
|
44
|
+
type: { type: 'string', description: 'Item type: "change" or "spec"' },
|
|
45
|
+
json: { type: 'boolean', description: 'Output as JSON', default: true }
|
|
46
|
+
},
|
|
47
|
+
required: ['itemName']
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'openspec_validate',
|
|
52
|
+
description: 'Validate a change proposal or spec',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
itemName: { type: 'string', description: 'Name of the change to validate (optional)' },
|
|
57
|
+
all: { type: 'boolean', description: 'Validate all changes and specs' },
|
|
58
|
+
strict: { type: 'boolean', description: 'Enable strict validation mode' },
|
|
59
|
+
json: { type: 'boolean', description: 'Output as JSON' }
|
|
60
|
+
},
|
|
61
|
+
required: []
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'openspec_archive',
|
|
66
|
+
description: 'Archive a completed change and update main specs',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
changeName: { type: 'string', description: 'Name of the change to archive' },
|
|
71
|
+
skipSpecs: { type: 'boolean', description: 'Skip spec updates' }
|
|
72
|
+
},
|
|
73
|
+
required: ['changeName']
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'openspec_new_change',
|
|
78
|
+
description: 'Create a new change directory',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
name: { type: 'string', description: 'Name of the change' },
|
|
83
|
+
description: { type: 'string', description: 'Description to add to README.md' }
|
|
84
|
+
},
|
|
85
|
+
required: ['name']
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'openspec_status',
|
|
90
|
+
description: 'Display artifact completion status for a change',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
changeName: { type: 'string', description: 'Change name to show status for' },
|
|
95
|
+
json: { type: 'boolean', description: 'Output as JSON', default: true }
|
|
96
|
+
},
|
|
97
|
+
required: []
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'openspec_instructions',
|
|
102
|
+
description: 'Output enriched instructions for an artifact or apply',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
artifact: { type: 'string', description: 'Artifact name (e.g. design.md, tasks.md) or "apply"' },
|
|
107
|
+
changeName: { type: 'string', description: 'Change name' },
|
|
108
|
+
json: { type: 'boolean', description: 'Output as JSON' }
|
|
109
|
+
},
|
|
110
|
+
required: ['artifact']
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
export async function handleToolCall(name, args, cwd) {
|
|
116
|
+
const runOpenSpec = async (callArgs) => {
|
|
117
|
+
const cmd = `${OPENSPEC_CMD} ${callArgs.join(' ')}`;
|
|
118
|
+
try {
|
|
119
|
+
const { stdout, stderr } = await execAsync(cmd, { cwd });
|
|
120
|
+
return { success: true, stdout, stderr, message: `Ran: ${cmd}` };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
stdout: error.stdout,
|
|
126
|
+
stderr: error.stderr,
|
|
127
|
+
message: `Command failed: ${error.message}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
switch (name) {
|
|
132
|
+
case 'openspec_init': {
|
|
133
|
+
return await runOpenSpec(['init', '--no-interactive']);
|
|
134
|
+
}
|
|
135
|
+
case 'openspec_update': {
|
|
136
|
+
return await runOpenSpec(['update']);
|
|
137
|
+
}
|
|
138
|
+
case 'openspec_list': {
|
|
139
|
+
const cmdArgs = ['list'];
|
|
140
|
+
if (args.specs)
|
|
141
|
+
cmdArgs.push('--specs');
|
|
142
|
+
if (args.json !== false)
|
|
143
|
+
cmdArgs.push('--json');
|
|
144
|
+
return await runOpenSpec(cmdArgs);
|
|
145
|
+
}
|
|
146
|
+
case 'openspec_show': {
|
|
147
|
+
const cmdArgs = ['show', `"${args.itemName}"`];
|
|
148
|
+
if (args.type)
|
|
149
|
+
cmdArgs.push('--type', args.type);
|
|
150
|
+
if (args.json !== false)
|
|
151
|
+
cmdArgs.push('--json');
|
|
152
|
+
return await runOpenSpec(cmdArgs);
|
|
153
|
+
}
|
|
154
|
+
case 'openspec_validate': {
|
|
155
|
+
const cmdArgs = ['validate'];
|
|
156
|
+
if (args.itemName)
|
|
157
|
+
cmdArgs.push(`"${args.itemName}"`);
|
|
158
|
+
if (args.all)
|
|
159
|
+
cmdArgs.push('--all');
|
|
160
|
+
if (args.strict)
|
|
161
|
+
cmdArgs.push('--strict');
|
|
162
|
+
if (args.json !== false)
|
|
163
|
+
cmdArgs.push('--json');
|
|
164
|
+
return await runOpenSpec(cmdArgs);
|
|
165
|
+
}
|
|
166
|
+
case 'openspec_archive': {
|
|
167
|
+
const cmdArgs = ['archive', `"${args.changeName}"`, '--yes'];
|
|
168
|
+
if (args.skipSpecs)
|
|
169
|
+
cmdArgs.push('--skip-specs');
|
|
170
|
+
return await runOpenSpec(cmdArgs);
|
|
171
|
+
}
|
|
172
|
+
case 'openspec_new_change': {
|
|
173
|
+
const cmdArgs = ['new', 'change', `"${args.name}"`];
|
|
174
|
+
if (args.description)
|
|
175
|
+
cmdArgs.push('--description', `"${args.description}"`);
|
|
176
|
+
return await runOpenSpec(cmdArgs);
|
|
177
|
+
}
|
|
178
|
+
case 'openspec_status': {
|
|
179
|
+
const cmdArgs = ['status'];
|
|
180
|
+
if (args.changeName)
|
|
181
|
+
cmdArgs.push('--change', `"${args.changeName}"`);
|
|
182
|
+
if (args.json !== false)
|
|
183
|
+
cmdArgs.push('--json');
|
|
184
|
+
return await runOpenSpec(cmdArgs);
|
|
185
|
+
}
|
|
186
|
+
case 'openspec_instructions': {
|
|
187
|
+
const cmdArgs = ['instructions', `"${args.artifact}"`];
|
|
188
|
+
if (args.changeName)
|
|
189
|
+
cmdArgs.push('--change', `"${args.changeName}"`);
|
|
190
|
+
if (args.json !== false)
|
|
191
|
+
cmdArgs.push('--json');
|
|
192
|
+
return await runOpenSpec(cmdArgs);
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return { success: false, message: `Unknown tool: ${name}` };
|
|
196
|
+
}
|
|
197
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@igor-olikh/openspec-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for OpenSpec",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"openspec-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
17
|
+
"zod": "^3.23.8"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.18.1",
|
|
21
|
+
"tsx": "^4.19.2",
|
|
22
|
+
"typescript": "^5.7.2"
|
|
23
|
+
}
|
|
24
|
+
}
|