@spekoai/mcp 1.0.4 → 1.0.6
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 +32 -122
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -20
- package/dist/init.d.ts +84 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +661 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,128 +1,42 @@
|
|
|
1
1
|
# @spekoai/mcp
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
{
|
|
11
|
-
"mcpServers": {
|
|
12
|
-
"spekoai": {
|
|
13
|
-
"url": "https://mcp.speko.ai/mcp-auth"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Use this package for stdio-only clients.
|
|
3
|
+
Interactive installer and local stdio-to-remote bridge for Speko MCP. The
|
|
4
|
+
installer configures Speko's hosted remote MCP endpoint in coding tools that
|
|
5
|
+
support it. The `bridge` command is only for MCP clients that require a local
|
|
6
|
+
stdio command: it speaks stdio to the client, connects to Speko's hosted MCP
|
|
7
|
+
server over HTTP, and does not contain Speko tool logic of its own.
|
|
20
8
|
|
|
21
9
|
## Install
|
|
22
10
|
|
|
23
11
|
```bash
|
|
24
|
-
npx @spekoai/mcp@latest
|
|
12
|
+
npx @spekoai/mcp@latest init
|
|
25
13
|
```
|
|
26
14
|
|
|
27
|
-
The package exposes the `spekoai-mcp` binary.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
OAuth-capable remote MCP clients should prefer the hosted endpoint:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
claude mcp add --transport http spekoai https://mcp.speko.ai/mcp-auth
|
|
35
|
-
```
|
|
15
|
+
The package exposes the `spekoai-mcp` binary. Run `init` for a guided setup
|
|
16
|
+
wizard that can configure Claude Code, Codex, OpenCode, Cursor, and generic MCP
|
|
17
|
+
clients.
|
|
36
18
|
|
|
37
|
-
For
|
|
19
|
+
For scripted setup, pass the choices explicitly:
|
|
38
20
|
|
|
39
21
|
```bash
|
|
40
|
-
|
|
22
|
+
npx @spekoai/mcp@latest init --access full --auth oauth --tools claude,codex --scope user --yes
|
|
41
23
|
```
|
|
42
24
|
|
|
43
|
-
|
|
44
|
-
client environment. The bridge forwards it as `Authorization: Bearer ...`.
|
|
45
|
-
|
|
46
|
-
## Cursor
|
|
25
|
+
## Bridge
|
|
47
26
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```json
|
|
51
|
-
{
|
|
52
|
-
"mcpServers": {
|
|
53
|
-
"spekoai": {
|
|
54
|
-
"command": "npx",
|
|
55
|
-
"args": ["-y", "@spekoai/mcp@latest"]
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
```
|
|
27
|
+
Most users should run `init`. Use `bridge` only when a client cannot connect to
|
|
28
|
+
remote MCP directly and asks for a local command-based MCP server.
|
|
60
29
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```json
|
|
64
|
-
{
|
|
65
|
-
"mcpServers": {
|
|
66
|
-
"spekoai": {
|
|
67
|
-
"command": "npx",
|
|
68
|
-
"args": ["-y", "@spekoai/mcp@latest"],
|
|
69
|
-
"env": {
|
|
70
|
-
"SPEKO_API_KEY": "sk_live_xxx"
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
30
|
+
```bash
|
|
31
|
+
npx @spekoai/mcp@latest bridge
|
|
75
32
|
```
|
|
76
33
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
{
|
|
83
|
-
"$schema": "https://opencode.ai/config.json",
|
|
84
|
-
"mcp": {
|
|
85
|
-
"spekoai": {
|
|
86
|
-
"type": "local",
|
|
87
|
-
"command": ["pnpm", "dlx", "@spekoai/mcp@latest"],
|
|
88
|
-
"enabled": true
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
34
|
+
For API-key auth in a headless bridge setup, provide `SPEKO_API_KEY` in the MCP
|
|
35
|
+
client environment. The bridge forwards it to the hosted MCP server as
|
|
36
|
+
`Authorization: Bearer ...`.
|
|
93
37
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```json
|
|
97
|
-
{
|
|
98
|
-
"$schema": "https://opencode.ai/config.json",
|
|
99
|
-
"mcp": {
|
|
100
|
-
"spekoai": {
|
|
101
|
-
"type": "local",
|
|
102
|
-
"command": ["pnpm", "dlx", "@spekoai/mcp@latest"],
|
|
103
|
-
"enabled": true,
|
|
104
|
-
"environment": {
|
|
105
|
-
"SPEKO_API_KEY": "{env:SPEKO_API_KEY}"
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Generic MCP Config
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"mcpServers": {
|
|
117
|
-
"spekoai": {
|
|
118
|
-
"command": "npx",
|
|
119
|
-
"args": ["-y", "@spekoai/mcp@latest"]
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## Configuration
|
|
38
|
+
The `bridge` command adapts local stdio MCP to Speko's remote HTTP MCP endpoint.
|
|
39
|
+
Use direct remote MCP configuration instead when your client supports it.
|
|
126
40
|
|
|
127
41
|
Defaults:
|
|
128
42
|
|
|
@@ -131,35 +45,31 @@ Defaults:
|
|
|
131
45
|
|
|
132
46
|
Environment variables:
|
|
133
47
|
|
|
134
|
-
- `SPEKOAI_MCP_AUTH_URL`: override the authenticated endpoint.
|
|
135
|
-
- `SPEKOAI_MCP_URL`: override the public endpoint.
|
|
136
48
|
- `SPEKO_API_KEY`: forward an API key as a bearer token.
|
|
137
49
|
|
|
138
50
|
CLI examples:
|
|
139
51
|
|
|
140
52
|
```bash
|
|
141
|
-
npx @spekoai/mcp@latest
|
|
142
|
-
npx @spekoai/mcp@latest --public
|
|
143
|
-
SPEKO_API_KEY=sk_live_xxx npx @spekoai/mcp@latest
|
|
144
|
-
SPEKOAI_MCP_AUTH_URL=https://mcp-staging.speko.dev/mcp-auth npx @spekoai/mcp@latest
|
|
145
|
-
npx @spekoai/mcp@latest https://mcp-staging.speko.dev/mcp-auth --debug
|
|
53
|
+
npx @spekoai/mcp@latest bridge
|
|
54
|
+
npx @spekoai/mcp@latest bridge --public
|
|
55
|
+
SPEKO_API_KEY=sk_live_xxx npx @spekoai/mcp@latest bridge
|
|
146
56
|
```
|
|
147
57
|
|
|
148
|
-
All remaining arguments are passed through to
|
|
58
|
+
All remaining arguments are passed through to
|
|
59
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote).
|
|
149
60
|
|
|
150
61
|
## Troubleshooting
|
|
151
62
|
|
|
152
63
|
Run:
|
|
153
64
|
|
|
154
65
|
```bash
|
|
155
|
-
npx @spekoai/mcp@latest --help
|
|
66
|
+
npx @spekoai/mcp@latest bridge --help
|
|
156
67
|
```
|
|
157
68
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
```
|
|
69
|
+
When using `bridge`, OAuth state is handled by
|
|
70
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote). It may create
|
|
71
|
+
`~/.mcp-auth` or use `MCP_REMOTE_CONFIG_DIR` to store local OAuth credentials
|
|
72
|
+
and debug logs. The `init` wizard does not write that directory.
|
|
163
73
|
|
|
164
74
|
For connection or OAuth issues, pass `--debug` and inspect the log path printed
|
|
165
|
-
by `mcp-remote
|
|
75
|
+
by [`mcp-remote`](https://www.npmjs.com/package/mcp-remote).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const DEFAULT_AUTH_MCP_URL = "https://mcp.speko.ai/mcp-auth";
|
|
2
|
+
export declare const DEFAULT_PUBLIC_MCP_URL = "https://mcp.speko.ai/mcp";
|
|
3
|
+
export declare const AUTH_HEADER_ENV = "SPEKOAI_MCP_AUTH_HEADER";
|
|
4
|
+
export type Environment = Record<string, string | undefined>;
|
|
5
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,kCAAkC,CAAC;AACpE,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AACjE,eAAO,MAAM,eAAe,4BAA4B,CAAC;AAEzD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export type Environment
|
|
2
|
+
import { AUTH_HEADER_ENV, DEFAULT_AUTH_MCP_URL, DEFAULT_PUBLIC_MCP_URL, type Environment } from './constants.js';
|
|
3
|
+
export type { InitAccess, InitAuth, InitScope, InitTool, ParsedInitArgs, PlannedInitStep, ResolvedInitOptions, } from './init.js';
|
|
4
|
+
export { buildCodexConfig, buildCursorConfig, buildInitPlan, buildOpenCodeConfig, completeInitArgs, DEFAULT_SCOPE, DEFAULT_SELECTED_TOOLS, endpointForAccess, INIT_HELP_TEXT, parseInitArgs, runInitCommand, } from './init.js';
|
|
5
|
+
export type { Environment };
|
|
6
|
+
export { AUTH_HEADER_ENV, DEFAULT_AUTH_MCP_URL, DEFAULT_PUBLIC_MCP_URL };
|
|
6
7
|
export type CliConfig = {
|
|
7
8
|
serverUrl: string;
|
|
8
9
|
passthroughArgs: string[];
|
|
@@ -13,7 +14,8 @@ export type AuthHeaderConfig = {
|
|
|
13
14
|
args: string[];
|
|
14
15
|
envValue?: string;
|
|
15
16
|
};
|
|
16
|
-
export declare const HELP_TEXT = "Usage: spekoai-mcp
|
|
17
|
+
export declare const HELP_TEXT = "Usage: spekoai-mcp <command> [options]\n\nConfigure Speko MCP in coding tools, or run the local stdio bridge for MCP clients that cannot connect to remote HTTP MCP directly.\n\nCommands:\n init Configure Speko MCP in coding tools.\n bridge Run the local stdio bridge to Speko's hosted MCP server.\n\nOptions:\n -h, --help Print this help text.\n\nExamples:\n spekoai-mcp init\n spekoai-mcp init --dry-run --access docs --tools cursor --scope project --yes\n spekoai-mcp bridge\n spekoai-mcp bridge --public\n";
|
|
18
|
+
export declare const BRIDGE_HELP_TEXT = "Usage: spekoai-mcp bridge [options] [mcp-remote flags]\n\nBridge local stdio MCP clients to Speko's hosted MCP server.\n\nDefaults:\n Full Speko account access: https://mcp.speko.ai/mcp-auth\n Public docs and scaffolds only: https://mcp.speko.ai/mcp\n\nOptions:\n --public Connect to the public docs/scaffolding endpoint.\n -h, --help Print this help text.\n\nEnvironment:\n SPEKO_API_KEY Forward as Authorization bearer token.\n\nExamples:\n spekoai-mcp bridge\n spekoai-mcp bridge --public\n SPEKO_API_KEY=sk_live_xxx spekoai-mcp bridge\n\nAll remaining arguments are passed through to mcp-remote.\n";
|
|
17
19
|
export declare function resolveCliConfig(argv: readonly string[], env?: Environment): CliConfig;
|
|
18
20
|
export declare function buildAuthHeaderArgs(env?: Environment): AuthHeaderConfig;
|
|
19
21
|
export declare function buildProxyArgv(options: {
|
|
@@ -24,5 +26,6 @@ export declare function buildProxyArgv(options: {
|
|
|
24
26
|
passthroughArgs: readonly string[];
|
|
25
27
|
}): string[];
|
|
26
28
|
export declare function resolveProxyPath(): string;
|
|
29
|
+
export declare function isCliEntrypoint(entrypointPath?: string, moduleUrl?: string): boolean;
|
|
27
30
|
export declare function run(argv?: string[], env?: NodeJS.ProcessEnv): Promise<void>;
|
|
28
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAKA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAKA,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,WAAW,EAAE,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,CAAC;AAEzE,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,SAAS,2hBAgBrB,CAAC;AAEF,eAAO,MAAM,gBAAgB,mnBAqB5B,CAAC;AAEF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,GAAE,WAAyB,GAC7B,SAAS,CAwBX;AAED,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,WAAyB,GAAG,gBAAgB,CAUpF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,MAAM,EAAE,CAQX;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,eAAe,CAC7B,cAAc,SAAkB,EAChC,SAAS,SAAkB,GAC1B,OAAO,CAWT;AAED,wBAAsB,GAAG,CAAC,IAAI,WAAwB,EAAE,GAAG,oBAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBxF"}
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
export
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { AUTH_HEADER_ENV, DEFAULT_AUTH_MCP_URL, DEFAULT_PUBLIC_MCP_URL, } from './constants.js';
|
|
6
|
+
import { runInitCommand } from './init.js';
|
|
7
|
+
export { buildCodexConfig, buildCursorConfig, buildInitPlan, buildOpenCodeConfig, completeInitArgs, DEFAULT_SCOPE, DEFAULT_SELECTED_TOOLS, endpointForAccess, INIT_HELP_TEXT, parseInitArgs, runInitCommand, } from './init.js';
|
|
8
|
+
export { AUTH_HEADER_ENV, DEFAULT_AUTH_MCP_URL, DEFAULT_PUBLIC_MCP_URL };
|
|
9
|
+
export const HELP_TEXT = `Usage: spekoai-mcp <command> [options]
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Configure Speko MCP in coding tools, or run the local stdio bridge for MCP clients that cannot connect to remote HTTP MCP directly.
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
init Configure Speko MCP in coding tools.
|
|
15
|
+
bridge Run the local stdio bridge to Speko's hosted MCP server.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-h, --help Print this help text.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
spekoai-mcp init
|
|
22
|
+
spekoai-mcp init --dry-run --access docs --tools cursor --scope project --yes
|
|
23
|
+
spekoai-mcp bridge
|
|
24
|
+
spekoai-mcp bridge --public
|
|
25
|
+
`;
|
|
26
|
+
export const BRIDGE_HELP_TEXT = `Usage: spekoai-mcp bridge [options] [mcp-remote flags]
|
|
27
|
+
|
|
28
|
+
Bridge local stdio MCP clients to Speko's hosted MCP server.
|
|
10
29
|
|
|
11
30
|
Defaults:
|
|
12
|
-
|
|
13
|
-
Public
|
|
31
|
+
Full Speko account access: ${DEFAULT_AUTH_MCP_URL}
|
|
32
|
+
Public docs and scaffolds only: ${DEFAULT_PUBLIC_MCP_URL}
|
|
14
33
|
|
|
15
34
|
Options:
|
|
16
|
-
--public
|
|
17
|
-
-h, --help
|
|
35
|
+
--public Connect to the public docs/scaffolding endpoint.
|
|
36
|
+
-h, --help Print this help text.
|
|
18
37
|
|
|
19
38
|
Environment:
|
|
20
|
-
|
|
21
|
-
SPEKOAI_MCP_URL Override the public endpoint.
|
|
22
|
-
SPEKO_API_KEY Forward as Authorization bearer token.
|
|
39
|
+
SPEKO_API_KEY Forward as Authorization bearer token.
|
|
23
40
|
|
|
24
41
|
Examples:
|
|
25
|
-
spekoai-mcp
|
|
26
|
-
spekoai-mcp --public
|
|
27
|
-
SPEKO_API_KEY=sk_live_xxx spekoai-mcp
|
|
28
|
-
SPEKOAI_MCP_AUTH_URL=https://mcp-staging.speko.dev/mcp-auth spekoai-mcp
|
|
29
|
-
spekoai-mcp https://mcp-staging.speko.dev/mcp-auth --debug
|
|
42
|
+
spekoai-mcp bridge
|
|
43
|
+
spekoai-mcp bridge --public
|
|
44
|
+
SPEKO_API_KEY=sk_live_xxx spekoai-mcp bridge
|
|
30
45
|
|
|
31
46
|
All remaining arguments are passed through to mcp-remote.
|
|
32
47
|
`;
|
|
@@ -75,10 +90,37 @@ export function buildProxyArgv(options) {
|
|
|
75
90
|
export function resolveProxyPath() {
|
|
76
91
|
return createRequire(import.meta.url).resolve('mcp-remote/dist/proxy.js');
|
|
77
92
|
}
|
|
93
|
+
export function isCliEntrypoint(entrypointPath = process.argv[1], moduleUrl = import.meta.url) {
|
|
94
|
+
if (!entrypointPath) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
98
|
+
try {
|
|
99
|
+
return realpathSync(entrypointPath) === realpathSync(modulePath);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return pathToFileURL(entrypointPath).href === moduleUrl;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
78
105
|
export async function run(argv = process.argv.slice(2), env = process.env) {
|
|
106
|
+
if (argv[0] === 'init') {
|
|
107
|
+
await runInitCommand(argv.slice(1), { env });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (argv[0] === 'bridge') {
|
|
111
|
+
await runBridgeCommand(argv.slice(1), env);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
115
|
+
process.stdout.write(HELP_TEXT);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Unknown command: ${argv[0]}. Run "spekoai-mcp --help" for usage.`);
|
|
119
|
+
}
|
|
120
|
+
async function runBridgeCommand(argv, env) {
|
|
79
121
|
const config = resolveCliConfig(argv, env);
|
|
80
122
|
if (config.help) {
|
|
81
|
-
process.stdout.write(
|
|
123
|
+
process.stdout.write(BRIDGE_HELP_TEXT);
|
|
82
124
|
return;
|
|
83
125
|
}
|
|
84
126
|
const auth = buildAuthHeaderArgs(env);
|
|
@@ -105,7 +147,7 @@ function envUrl(value, fallback) {
|
|
|
105
147
|
function isHttpUrl(value) {
|
|
106
148
|
return value?.startsWith('https://') === true || value?.startsWith('http://') === true;
|
|
107
149
|
}
|
|
108
|
-
if (
|
|
150
|
+
if (isCliEntrypoint()) {
|
|
109
151
|
run().catch((error) => {
|
|
110
152
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
111
153
|
process.exit(1);
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type Environment } from './constants.js';
|
|
2
|
+
export type InitAccess = 'full' | 'docs';
|
|
3
|
+
export type InitAuth = 'oauth' | 'api-key';
|
|
4
|
+
export type InitScope = 'user' | 'project';
|
|
5
|
+
export type InitTool = 'claude' | 'codex' | 'opencode' | 'cursor' | 'other';
|
|
6
|
+
export type ParsedInitArgs = {
|
|
7
|
+
access?: InitAccess;
|
|
8
|
+
auth?: InitAuth;
|
|
9
|
+
tools?: InitTool[];
|
|
10
|
+
scope?: InitScope;
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
yes: boolean;
|
|
13
|
+
help: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ResolvedInitOptions = {
|
|
16
|
+
access: InitAccess;
|
|
17
|
+
auth?: InitAuth;
|
|
18
|
+
tools: InitTool[];
|
|
19
|
+
scope: InitScope;
|
|
20
|
+
dryRun: boolean;
|
|
21
|
+
yes: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type InitPaths = {
|
|
24
|
+
homeDir: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
};
|
|
27
|
+
export type FileUpdateResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
content: string;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
manualSnippet: string;
|
|
34
|
+
};
|
|
35
|
+
export type PlannedInitStep = {
|
|
36
|
+
kind: 'command';
|
|
37
|
+
tool: 'claude';
|
|
38
|
+
label: string;
|
|
39
|
+
command: string[];
|
|
40
|
+
manualSnippet: string;
|
|
41
|
+
postInstall?: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'file';
|
|
44
|
+
tool: 'codex' | 'opencode' | 'cursor';
|
|
45
|
+
label: string;
|
|
46
|
+
path: string;
|
|
47
|
+
build: (existing: string | undefined) => FileUpdateResult;
|
|
48
|
+
manualSnippet: string;
|
|
49
|
+
postInstall?: string;
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'manual';
|
|
52
|
+
tool: InitTool;
|
|
53
|
+
label: string;
|
|
54
|
+
manualSnippet: string;
|
|
55
|
+
postInstall?: string;
|
|
56
|
+
};
|
|
57
|
+
type InitDependencies = {
|
|
58
|
+
env?: Environment;
|
|
59
|
+
homeDir?: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
stdin?: NodeJS.ReadStream;
|
|
62
|
+
stdout?: NodeJS.WriteStream;
|
|
63
|
+
stderr?: NodeJS.WriteStream;
|
|
64
|
+
timestamp?: () => string;
|
|
65
|
+
runCommand?: (command: readonly string[]) => {
|
|
66
|
+
ok: boolean;
|
|
67
|
+
message: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare const DEFAULT_SELECTED_TOOLS: readonly InitTool[];
|
|
71
|
+
export declare const DEFAULT_SCOPE: InitScope;
|
|
72
|
+
export declare const INIT_HELP_TEXT = "Usage: spekoai-mcp init [options]\n\nConfigure Speko MCP in Claude Code, Codex, OpenCode, Cursor, or another MCP client.\n\nOptions:\n --access <full|docs> full uses https://mcp.speko.ai/mcp-auth; docs uses https://mcp.speko.ai/mcp\n --auth <oauth|api-key> Authentication mode for full access.\n --tools <list> Comma-separated tools: claude,codex,opencode,cursor,other\n --scope <user|project> Install globally for the user or in the current project.\n --dry-run Print the planned changes without writing files or running commands.\n --yes Skip the final confirmation prompt.\n -h, --help Print this help text.\n\nExamples:\n spekoai-mcp init\n spekoai-mcp init --dry-run --access docs --tools cursor --scope project --yes\n spekoai-mcp init --access full --auth oauth --tools claude,codex --scope user --yes\n";
|
|
73
|
+
export declare function parseInitArgs(argv: readonly string[]): ParsedInitArgs;
|
|
74
|
+
export declare function completeInitArgs(parsed: ParsedInitArgs): ResolvedInitOptions;
|
|
75
|
+
export declare function endpointForAccess(access: InitAccess): string;
|
|
76
|
+
export declare function isApiKeyAuth(options: Pick<ResolvedInitOptions, 'access' | 'auth'>): boolean;
|
|
77
|
+
export declare function buildCursorConfig(existing: string | undefined, endpoint: string, apiKeyAuth: boolean): FileUpdateResult;
|
|
78
|
+
export declare function buildOpenCodeConfig(existing: string | undefined, endpoint: string, apiKeyAuth: boolean): FileUpdateResult;
|
|
79
|
+
export declare function buildCodexConfig(existing: string | undefined, endpoint: string, apiKeyAuth: boolean): FileUpdateResult;
|
|
80
|
+
export declare function buildInitPlan(options: ResolvedInitOptions, paths: InitPaths): PlannedInitStep[];
|
|
81
|
+
export declare function renderPlanSummary(options: ResolvedInitOptions, steps: readonly PlannedInitStep[]): string;
|
|
82
|
+
export declare function runInitCommand(argv?: readonly string[], deps?: InitDependencies): Promise<void>;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { copyFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { confirm, intro, isCancel, log, multiselect, note, outro, select } from '@clack/prompts';
|
|
7
|
+
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser';
|
|
8
|
+
import { parse as parseToml } from 'smol-toml';
|
|
9
|
+
import { DEFAULT_AUTH_MCP_URL, DEFAULT_PUBLIC_MCP_URL } from './constants.js';
|
|
10
|
+
const TOOL_SPECS = [
|
|
11
|
+
{ tool: 'claude', label: 'Claude Code' },
|
|
12
|
+
{ tool: 'codex', label: 'Codex' },
|
|
13
|
+
{ tool: 'opencode', label: 'OpenCode' },
|
|
14
|
+
{ tool: 'cursor', label: 'Cursor' },
|
|
15
|
+
{ tool: 'other', label: 'Other clients' },
|
|
16
|
+
];
|
|
17
|
+
export const DEFAULT_SELECTED_TOOLS = ['claude', 'opencode'];
|
|
18
|
+
export const DEFAULT_SCOPE = 'project';
|
|
19
|
+
const TOOL_LABELS = new Map(TOOL_SPECS.map((spec) => [spec.tool, spec.label]));
|
|
20
|
+
export const INIT_HELP_TEXT = `Usage: spekoai-mcp init [options]
|
|
21
|
+
|
|
22
|
+
Configure Speko MCP in Claude Code, Codex, OpenCode, Cursor, or another MCP client.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--access <full|docs> full uses ${DEFAULT_AUTH_MCP_URL}; docs uses ${DEFAULT_PUBLIC_MCP_URL}
|
|
26
|
+
--auth <oauth|api-key> Authentication mode for full access.
|
|
27
|
+
--tools <list> Comma-separated tools: claude,codex,opencode,cursor,other
|
|
28
|
+
--scope <user|project> Install globally for the user or in the current project.
|
|
29
|
+
--dry-run Print the planned changes without writing files or running commands.
|
|
30
|
+
--yes Skip the final confirmation prompt.
|
|
31
|
+
-h, --help Print this help text.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
spekoai-mcp init
|
|
35
|
+
spekoai-mcp init --dry-run --access docs --tools cursor --scope project --yes
|
|
36
|
+
spekoai-mcp init --access full --auth oauth --tools claude,codex --scope user --yes
|
|
37
|
+
`;
|
|
38
|
+
export function parseInitArgs(argv) {
|
|
39
|
+
const parsed = { dryRun: false, yes: false, help: false };
|
|
40
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
41
|
+
const arg = argv[index];
|
|
42
|
+
switch (arg) {
|
|
43
|
+
case '--dry-run':
|
|
44
|
+
parsed.dryRun = true;
|
|
45
|
+
break;
|
|
46
|
+
case '--yes':
|
|
47
|
+
case '-y':
|
|
48
|
+
parsed.yes = true;
|
|
49
|
+
break;
|
|
50
|
+
case '--help':
|
|
51
|
+
case '-h':
|
|
52
|
+
parsed.help = true;
|
|
53
|
+
break;
|
|
54
|
+
case '--access':
|
|
55
|
+
parsed.access = parseAccess(readFlagValue(argv, index, arg));
|
|
56
|
+
index += 1;
|
|
57
|
+
break;
|
|
58
|
+
case '--auth':
|
|
59
|
+
parsed.auth = parseAuth(readFlagValue(argv, index, arg));
|
|
60
|
+
index += 1;
|
|
61
|
+
break;
|
|
62
|
+
case '--tools':
|
|
63
|
+
parsed.tools = parseTools(readFlagValue(argv, index, arg));
|
|
64
|
+
index += 1;
|
|
65
|
+
break;
|
|
66
|
+
case '--scope':
|
|
67
|
+
parsed.scope = parseScope(readFlagValue(argv, index, arg));
|
|
68
|
+
index += 1;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
if (arg.startsWith('--access=')) {
|
|
72
|
+
parsed.access = parseAccess(readInlineFlagValue(arg));
|
|
73
|
+
}
|
|
74
|
+
else if (arg.startsWith('--auth=')) {
|
|
75
|
+
parsed.auth = parseAuth(readInlineFlagValue(arg));
|
|
76
|
+
}
|
|
77
|
+
else if (arg.startsWith('--tools=')) {
|
|
78
|
+
parsed.tools = parseTools(readInlineFlagValue(arg));
|
|
79
|
+
}
|
|
80
|
+
else if (arg.startsWith('--scope=')) {
|
|
81
|
+
parsed.scope = parseScope(readInlineFlagValue(arg));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error(`Unknown init option: ${arg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
export function completeInitArgs(parsed) {
|
|
91
|
+
const missing = [];
|
|
92
|
+
if (!parsed.access)
|
|
93
|
+
missing.push('--access');
|
|
94
|
+
if (parsed.access === 'full' && !parsed.auth)
|
|
95
|
+
missing.push('--auth');
|
|
96
|
+
if (!parsed.tools?.length)
|
|
97
|
+
missing.push('--tools');
|
|
98
|
+
if (!parsed.scope)
|
|
99
|
+
missing.push('--scope');
|
|
100
|
+
if (!parsed.dryRun && !parsed.yes)
|
|
101
|
+
missing.push('--yes or --dry-run');
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
throw new Error(`spekoai-mcp init is running non-interactively. Provide ${missing.join(', ')} or run it in an interactive terminal.`);
|
|
104
|
+
}
|
|
105
|
+
const access = parsed.access;
|
|
106
|
+
const tools = parsed.tools;
|
|
107
|
+
const scope = parsed.scope;
|
|
108
|
+
if (!access || !tools?.length || !scope) {
|
|
109
|
+
throw new Error('spekoai-mcp init options are incomplete.');
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
access,
|
|
113
|
+
auth: access === 'full' ? parsed.auth : undefined,
|
|
114
|
+
tools,
|
|
115
|
+
scope,
|
|
116
|
+
dryRun: parsed.dryRun,
|
|
117
|
+
yes: parsed.yes,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function endpointForAccess(access) {
|
|
121
|
+
return access === 'full' ? DEFAULT_AUTH_MCP_URL : DEFAULT_PUBLIC_MCP_URL;
|
|
122
|
+
}
|
|
123
|
+
export function isApiKeyAuth(options) {
|
|
124
|
+
return options.access === 'full' && options.auth === 'api-key';
|
|
125
|
+
}
|
|
126
|
+
export function buildCursorConfig(existing, endpoint, apiKeyAuth) {
|
|
127
|
+
const server = apiKeyAuth
|
|
128
|
+
? {
|
|
129
|
+
url: endpoint,
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer $${'{env:SPEKO_API_KEY}'}`,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
: { url: endpoint };
|
|
135
|
+
return updateJsonc(existing, ['mcpServers', 'speko'], server, cursorSnippet(endpoint, apiKeyAuth));
|
|
136
|
+
}
|
|
137
|
+
export function buildOpenCodeConfig(existing, endpoint, apiKeyAuth) {
|
|
138
|
+
const server = apiKeyAuth
|
|
139
|
+
? {
|
|
140
|
+
type: 'remote',
|
|
141
|
+
url: endpoint,
|
|
142
|
+
oauth: false,
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: 'Bearer {env:SPEKO_API_KEY}',
|
|
145
|
+
},
|
|
146
|
+
enabled: true,
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
type: 'remote',
|
|
150
|
+
url: endpoint,
|
|
151
|
+
enabled: true,
|
|
152
|
+
};
|
|
153
|
+
const withSchema = updateJsonc(existing, ['$schema'], 'https://opencode.ai/config.json', openCodeSnippet(endpoint, apiKeyAuth));
|
|
154
|
+
if (!withSchema.ok)
|
|
155
|
+
return withSchema;
|
|
156
|
+
return updateJsonc(withSchema.content, ['mcp', 'speko'], server, openCodeSnippet(endpoint, apiKeyAuth));
|
|
157
|
+
}
|
|
158
|
+
export function buildCodexConfig(existing, endpoint, apiKeyAuth) {
|
|
159
|
+
const source = existing ?? '';
|
|
160
|
+
try {
|
|
161
|
+
parseToml(source || '');
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: `Could not parse existing TOML: ${error instanceof Error ? error.message : String(error)}`,
|
|
167
|
+
manualSnippet: codexSnippet(endpoint, apiKeyAuth),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const nextBlock = codexSnippet(endpoint, apiKeyAuth);
|
|
171
|
+
const withoutExisting = source.replace(/(^|\r?\n)\[mcp_servers\.speko\]\r?\n(?:(?!\r?\n\[).)*(?:\r?\n)?/s, '$1');
|
|
172
|
+
const trimmed = withoutExisting.replace(/\s+$/, '');
|
|
173
|
+
const content = `${trimmed ? `${trimmed}\n\n` : ''}${nextBlock}\n`;
|
|
174
|
+
try {
|
|
175
|
+
parseToml(content);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
reason: `Generated TOML failed validation: ${error instanceof Error ? error.message : String(error)}`,
|
|
181
|
+
manualSnippet: nextBlock,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, content };
|
|
185
|
+
}
|
|
186
|
+
export function buildInitPlan(options, paths) {
|
|
187
|
+
const endpoint = endpointForAccess(options.access);
|
|
188
|
+
const apiKeyAuth = isApiKeyAuth(options);
|
|
189
|
+
const steps = [];
|
|
190
|
+
for (const tool of options.tools) {
|
|
191
|
+
if (tool === 'claude') {
|
|
192
|
+
steps.push(buildClaudeStep(options, endpoint, apiKeyAuth));
|
|
193
|
+
}
|
|
194
|
+
else if (tool === 'codex') {
|
|
195
|
+
steps.push({
|
|
196
|
+
kind: 'file',
|
|
197
|
+
tool,
|
|
198
|
+
label: 'Codex config',
|
|
199
|
+
path: join(paths.homeDir, '.codex', 'config.toml'),
|
|
200
|
+
build: (existing) => buildCodexConfig(existing, endpoint, apiKeyAuth),
|
|
201
|
+
manualSnippet: codexSnippet(endpoint, apiKeyAuth),
|
|
202
|
+
postInstall: options.access === 'full' && !apiKeyAuth ? 'Run: codex mcp login speko' : undefined,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else if (tool === 'opencode') {
|
|
206
|
+
const configPath = options.scope === 'user'
|
|
207
|
+
? join(paths.homeDir, '.config', 'opencode', 'opencode.json')
|
|
208
|
+
: join(paths.cwd, 'opencode.json');
|
|
209
|
+
steps.push({
|
|
210
|
+
kind: 'file',
|
|
211
|
+
tool,
|
|
212
|
+
label: 'OpenCode config',
|
|
213
|
+
path: configPath,
|
|
214
|
+
build: (existing) => buildOpenCodeConfig(existing, endpoint, apiKeyAuth),
|
|
215
|
+
manualSnippet: openCodeSnippet(endpoint, apiKeyAuth),
|
|
216
|
+
postInstall: options.access === 'full' && !apiKeyAuth ? 'Run: opencode mcp auth speko' : undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (tool === 'cursor') {
|
|
220
|
+
const configPath = options.scope === 'user'
|
|
221
|
+
? join(paths.homeDir, '.cursor', 'mcp.json')
|
|
222
|
+
: join(paths.cwd, '.cursor', 'mcp.json');
|
|
223
|
+
steps.push({
|
|
224
|
+
kind: 'file',
|
|
225
|
+
tool,
|
|
226
|
+
label: 'Cursor config',
|
|
227
|
+
path: configPath,
|
|
228
|
+
build: (existing) => buildCursorConfig(existing, endpoint, apiKeyAuth),
|
|
229
|
+
manualSnippet: cursorSnippet(endpoint, apiKeyAuth),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
steps.push({
|
|
234
|
+
kind: 'manual',
|
|
235
|
+
tool,
|
|
236
|
+
label: 'Other MCP clients',
|
|
237
|
+
manualSnippet: otherClientSnippet(endpoint, options.access, apiKeyAuth),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return steps;
|
|
242
|
+
}
|
|
243
|
+
export function renderPlanSummary(options, steps) {
|
|
244
|
+
const endpoint = endpointForAccess(options.access);
|
|
245
|
+
const lines = [
|
|
246
|
+
`Access: ${options.access === 'full' ? 'Full access' : 'Docs and scaffolds only'}`,
|
|
247
|
+
`Endpoint: ${endpoint}`,
|
|
248
|
+
`Auth: ${options.access === 'full' ? (options.auth === 'api-key' ? 'SPEKO_API_KEY' : 'OAuth') : 'None'}`,
|
|
249
|
+
`Scope: ${options.scope}`,
|
|
250
|
+
'',
|
|
251
|
+
'Planned changes:',
|
|
252
|
+
...steps.map((step) => {
|
|
253
|
+
if (step.kind === 'command') {
|
|
254
|
+
return `- ${step.label}: run ${formatCommand(step.command)}`;
|
|
255
|
+
}
|
|
256
|
+
if (step.kind === 'file') {
|
|
257
|
+
return `- ${step.label}: update ${step.path}`;
|
|
258
|
+
}
|
|
259
|
+
return `- ${step.label}: print manual Streamable HTTP settings`;
|
|
260
|
+
}),
|
|
261
|
+
];
|
|
262
|
+
const postInstall = steps
|
|
263
|
+
.map((step) => step.postInstall)
|
|
264
|
+
.filter((value) => Boolean(value));
|
|
265
|
+
if (postInstall.length > 0) {
|
|
266
|
+
lines.push('', 'After configuring:', ...postInstall.map((step) => `- ${step}`));
|
|
267
|
+
}
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
}
|
|
270
|
+
export async function runInitCommand(argv = process.argv.slice(2), deps = {}) {
|
|
271
|
+
const env = deps.env ?? process.env;
|
|
272
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
273
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
274
|
+
const parsed = parseInitArgs(argv);
|
|
275
|
+
if (parsed.help) {
|
|
276
|
+
stdout.write(INIT_HELP_TEXT);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const interactive = Boolean((deps.stdin ?? process.stdin).isTTY && stdout.isTTY);
|
|
280
|
+
const options = interactive ? await promptForMissingOptions(parsed) : completeInitArgs(parsed);
|
|
281
|
+
const paths = {
|
|
282
|
+
homeDir: deps.homeDir ?? env.HOME ?? homedir(),
|
|
283
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
284
|
+
};
|
|
285
|
+
const steps = buildInitPlan(options, paths);
|
|
286
|
+
const summary = renderPlanSummary(options, steps);
|
|
287
|
+
if (interactive) {
|
|
288
|
+
note(summary, options.dryRun ? 'Dry run' : 'Ready to configure Speko MCP');
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
stdout.write(`${summary}\n`);
|
|
292
|
+
}
|
|
293
|
+
if (options.dryRun) {
|
|
294
|
+
stdout.write(`${renderManualSnippets(steps)}\n`);
|
|
295
|
+
printApiKeyReminder(options, env, stdout);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!options.yes) {
|
|
299
|
+
const shouldApply = await confirm({ message: 'Apply these changes?' });
|
|
300
|
+
if (isCancel(shouldApply) || !shouldApply) {
|
|
301
|
+
cancelInit('No changes applied.');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const applied = await applyInitPlan(steps, {
|
|
306
|
+
timestamp: deps.timestamp ?? defaultTimestamp,
|
|
307
|
+
runCommand: deps.runCommand ?? runExternalCommand,
|
|
308
|
+
});
|
|
309
|
+
const resultText = renderAppliedSteps(applied);
|
|
310
|
+
if (interactive) {
|
|
311
|
+
outro(resultText);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
stdout.write(`${resultText}\n`);
|
|
315
|
+
}
|
|
316
|
+
stdout.write(`${renderManualSnippets(steps.filter((step) => step.kind === 'manual'))}\n`);
|
|
317
|
+
stdout.write(`${renderFailedManualSnippets(applied)}\n`);
|
|
318
|
+
printApiKeyReminder(options, env, stdout);
|
|
319
|
+
const failures = applied.filter((step) => !step.ok);
|
|
320
|
+
if (failures.length > 0) {
|
|
321
|
+
stderr.write(`Some selected tools were not configured automatically. Use the printed manual snippets for those tools.\n`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function promptForMissingOptions(parsed) {
|
|
325
|
+
intro('Configure Speko MCP');
|
|
326
|
+
const access = parsed.access ??
|
|
327
|
+
(await promptValue(select({
|
|
328
|
+
message: 'Which Speko MCP endpoint do you want?',
|
|
329
|
+
options: [
|
|
330
|
+
{
|
|
331
|
+
value: 'full',
|
|
332
|
+
label: 'Full Speko account access',
|
|
333
|
+
hint: 'private actions like balance, logs, builds, tests, deploys, plus docs',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
value: 'docs',
|
|
337
|
+
label: 'Public docs and scaffolds only',
|
|
338
|
+
hint: 'no sign-in; docs, package guidance, recommendations, and example scaffolds',
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
})));
|
|
342
|
+
const auth = access === 'full'
|
|
343
|
+
? (parsed.auth ??
|
|
344
|
+
(await promptValue(select({
|
|
345
|
+
message: 'How should full access authenticate?',
|
|
346
|
+
options: [
|
|
347
|
+
{ value: 'oauth', label: 'OAuth', hint: 'recommended when your tool supports it' },
|
|
348
|
+
{ value: 'api-key', label: 'SPEKO_API_KEY', hint: 'uses an environment variable' },
|
|
349
|
+
],
|
|
350
|
+
}))))
|
|
351
|
+
: undefined;
|
|
352
|
+
const tools = parsed.tools ??
|
|
353
|
+
(await promptValue(multiselect({
|
|
354
|
+
message: 'Which coding tools should be configured?',
|
|
355
|
+
required: true,
|
|
356
|
+
initialValues: [...DEFAULT_SELECTED_TOOLS],
|
|
357
|
+
options: TOOL_SPECS.map((spec) => ({ value: spec.tool, label: spec.label })),
|
|
358
|
+
})));
|
|
359
|
+
const scope = parsed.scope ??
|
|
360
|
+
(await promptValue(select({
|
|
361
|
+
message: 'Where should supported configs be written?',
|
|
362
|
+
initialValue: DEFAULT_SCOPE,
|
|
363
|
+
options: [
|
|
364
|
+
{ value: 'project', label: 'Current project config' },
|
|
365
|
+
{ value: 'user', label: 'Global/user config' },
|
|
366
|
+
],
|
|
367
|
+
})));
|
|
368
|
+
return {
|
|
369
|
+
access,
|
|
370
|
+
auth,
|
|
371
|
+
tools,
|
|
372
|
+
scope,
|
|
373
|
+
dryRun: parsed.dryRun,
|
|
374
|
+
yes: parsed.yes,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function promptValue(value) {
|
|
378
|
+
const resolved = await value;
|
|
379
|
+
if (isCancel(resolved)) {
|
|
380
|
+
cancelInit('No changes applied.');
|
|
381
|
+
throw new Error('Init cancelled.');
|
|
382
|
+
}
|
|
383
|
+
return resolved;
|
|
384
|
+
}
|
|
385
|
+
function cancelInit(message) {
|
|
386
|
+
log.warn(message);
|
|
387
|
+
outro('Cancelled');
|
|
388
|
+
}
|
|
389
|
+
async function applyInitPlan(steps, deps) {
|
|
390
|
+
const applied = [];
|
|
391
|
+
const timestamp = deps.timestamp();
|
|
392
|
+
for (const step of steps) {
|
|
393
|
+
if (step.kind === 'manual') {
|
|
394
|
+
applied.push({ label: step.label, ok: true, message: 'Manual instructions printed.' });
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (step.kind === 'command') {
|
|
398
|
+
const result = deps.runCommand(step.command);
|
|
399
|
+
applied.push({
|
|
400
|
+
label: step.label,
|
|
401
|
+
ok: result.ok,
|
|
402
|
+
message: result.message,
|
|
403
|
+
manualSnippet: result.ok ? undefined : step.manualSnippet,
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const existing = existsSync(step.path) ? readFileSync(step.path, 'utf8') : undefined;
|
|
408
|
+
const next = step.build(existing);
|
|
409
|
+
if (!next.ok) {
|
|
410
|
+
applied.push({
|
|
411
|
+
label: step.label,
|
|
412
|
+
ok: false,
|
|
413
|
+
message: next.reason,
|
|
414
|
+
manualSnippet: next.manualSnippet,
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (existing === next.content) {
|
|
419
|
+
applied.push({ label: step.label, ok: true, message: `Already up to date: ${step.path}` });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
mkdirSync(dirname(step.path), { recursive: true });
|
|
423
|
+
if (existing !== undefined) {
|
|
424
|
+
const backupPath = `${step.path}.${timestamp}.bak`;
|
|
425
|
+
await copyFile(step.path, backupPath);
|
|
426
|
+
}
|
|
427
|
+
writeFileSync(step.path, next.content);
|
|
428
|
+
applied.push({ label: step.label, ok: true, message: `Updated ${step.path}` });
|
|
429
|
+
}
|
|
430
|
+
return applied;
|
|
431
|
+
}
|
|
432
|
+
function buildClaudeStep(options, endpoint, apiKeyAuth) {
|
|
433
|
+
const manualSnippet = claudeSnippet(endpoint, options.scope, apiKeyAuth);
|
|
434
|
+
if (apiKeyAuth) {
|
|
435
|
+
return {
|
|
436
|
+
kind: 'manual',
|
|
437
|
+
tool: 'claude',
|
|
438
|
+
label: 'Claude Code',
|
|
439
|
+
manualSnippet,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
kind: 'command',
|
|
444
|
+
tool: 'claude',
|
|
445
|
+
label: 'Claude Code',
|
|
446
|
+
command: [
|
|
447
|
+
'claude',
|
|
448
|
+
'mcp',
|
|
449
|
+
'add',
|
|
450
|
+
'--transport',
|
|
451
|
+
'http',
|
|
452
|
+
'--scope',
|
|
453
|
+
options.scope,
|
|
454
|
+
'speko',
|
|
455
|
+
endpoint,
|
|
456
|
+
],
|
|
457
|
+
manualSnippet,
|
|
458
|
+
postInstall: options.access === 'full' ? 'In Claude Code, run /mcp and complete sign-in.' : undefined,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function updateJsonc(existing, path, value, manualSnippet) {
|
|
462
|
+
const source = existing?.trim() ? existing : '{}\n';
|
|
463
|
+
const errors = [];
|
|
464
|
+
const parsed = parseJsonc(source, errors, { allowTrailingComma: true, disallowComments: false });
|
|
465
|
+
if (errors.length > 0) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
reason: `Could not parse existing JSON/JSONC config.`,
|
|
469
|
+
manualSnippet,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (!isRecord(parsed)) {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
reason: 'Existing JSON/JSONC config root is not an object.',
|
|
476
|
+
manualSnippet,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const edits = modify(source, [...path], value, {
|
|
480
|
+
formattingOptions: {
|
|
481
|
+
insertSpaces: true,
|
|
482
|
+
tabSize: 2,
|
|
483
|
+
eol: '\n',
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
return { ok: true, content: ensureTrailingNewline(applyEdits(source, edits)) };
|
|
487
|
+
}
|
|
488
|
+
function codexSnippet(endpoint, apiKeyAuth) {
|
|
489
|
+
return `[mcp_servers.speko]
|
|
490
|
+
url = "${endpoint}"${apiKeyAuth ? '\nbearer_token_env_var = "SPEKO_API_KEY"' : ''}`;
|
|
491
|
+
}
|
|
492
|
+
function openCodeSnippet(endpoint, apiKeyAuth) {
|
|
493
|
+
const server = apiKeyAuth
|
|
494
|
+
? `{
|
|
495
|
+
"type": "remote",
|
|
496
|
+
"url": "${endpoint}",
|
|
497
|
+
"oauth": false,
|
|
498
|
+
"headers": {
|
|
499
|
+
"Authorization": "Bearer {env:SPEKO_API_KEY}"
|
|
500
|
+
},
|
|
501
|
+
"enabled": true
|
|
502
|
+
}`
|
|
503
|
+
: `{
|
|
504
|
+
"type": "remote",
|
|
505
|
+
"url": "${endpoint}",
|
|
506
|
+
"enabled": true
|
|
507
|
+
}`;
|
|
508
|
+
return `{
|
|
509
|
+
"$schema": "https://opencode.ai/config.json",
|
|
510
|
+
"mcp": {
|
|
511
|
+
"speko": ${server}
|
|
512
|
+
}
|
|
513
|
+
}`;
|
|
514
|
+
}
|
|
515
|
+
function cursorSnippet(endpoint, apiKeyAuth) {
|
|
516
|
+
const server = apiKeyAuth
|
|
517
|
+
? `{
|
|
518
|
+
"url": "${endpoint}",
|
|
519
|
+
"headers": {
|
|
520
|
+
"Authorization": "Bearer $${'{env:SPEKO_API_KEY}'}"
|
|
521
|
+
}
|
|
522
|
+
}`
|
|
523
|
+
: `{
|
|
524
|
+
"url": "${endpoint}"
|
|
525
|
+
}`;
|
|
526
|
+
return `{
|
|
527
|
+
"mcpServers": {
|
|
528
|
+
"speko": ${server}
|
|
529
|
+
}
|
|
530
|
+
}`;
|
|
531
|
+
}
|
|
532
|
+
function claudeSnippet(endpoint, scope, apiKeyAuth) {
|
|
533
|
+
return apiKeyAuth
|
|
534
|
+
? `claude mcp add --transport http --scope ${scope} speko ${endpoint} \\
|
|
535
|
+
--header "Authorization: Bearer sk_live_xxx"`
|
|
536
|
+
: `claude mcp add --transport http --scope ${scope} speko ${endpoint}`;
|
|
537
|
+
}
|
|
538
|
+
function otherClientSnippet(endpoint, access, apiKeyAuth) {
|
|
539
|
+
const auth = access === 'docs'
|
|
540
|
+
? 'Authentication: None'
|
|
541
|
+
: apiKeyAuth
|
|
542
|
+
? 'API key auth: Send Authorization: Bearer sk_live_xxx'
|
|
543
|
+
: "OAuth: Use the client's OAuth flow";
|
|
544
|
+
return `Name: speko
|
|
545
|
+
URL: ${endpoint}
|
|
546
|
+
${auth}`;
|
|
547
|
+
}
|
|
548
|
+
function renderManualSnippets(steps) {
|
|
549
|
+
const snippets = steps
|
|
550
|
+
.filter((step) => step.kind === 'manual')
|
|
551
|
+
.map((step) => `${step.label}:\n${step.manualSnippet}`);
|
|
552
|
+
return snippets.length > 0 ? `\nManual steps:\n\n${snippets.join('\n\n')}\n` : '';
|
|
553
|
+
}
|
|
554
|
+
function renderAppliedSteps(steps) {
|
|
555
|
+
return steps
|
|
556
|
+
.map((step) => `${step.ok ? 'OK' : 'SKIP'} ${step.label}: ${step.message}`)
|
|
557
|
+
.join('\n');
|
|
558
|
+
}
|
|
559
|
+
function renderFailedManualSnippets(steps) {
|
|
560
|
+
const snippets = steps
|
|
561
|
+
.filter((step) => !step.ok && step.manualSnippet)
|
|
562
|
+
.map((step) => `${step.label}:\n${step.manualSnippet}`);
|
|
563
|
+
return snippets.length > 0 ? `\nManual fallback:\n\n${snippets.join('\n\n')}\n` : '';
|
|
564
|
+
}
|
|
565
|
+
function printApiKeyReminder(options, env, stdout) {
|
|
566
|
+
if (!isApiKeyAuth(options) || env.SPEKO_API_KEY?.trim()) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
stdout.write(`\nSPEKO_API_KEY is not set. Before using API-key auth, run:\n${apiKeyExportCommand(env.SHELL)}\n`);
|
|
570
|
+
}
|
|
571
|
+
function apiKeyExportCommand(shell) {
|
|
572
|
+
if (shell?.endsWith('/fish')) {
|
|
573
|
+
return 'set -gx SPEKO_API_KEY sk_live_xxx';
|
|
574
|
+
}
|
|
575
|
+
if (process.platform === 'win32') {
|
|
576
|
+
return '$env:SPEKO_API_KEY = "sk_live_xxx"';
|
|
577
|
+
}
|
|
578
|
+
return 'export SPEKO_API_KEY=sk_live_xxx';
|
|
579
|
+
}
|
|
580
|
+
function runExternalCommand(command) {
|
|
581
|
+
const result = spawnSync(command[0] ?? '', command.slice(1), {
|
|
582
|
+
encoding: 'utf8',
|
|
583
|
+
stdio: 'pipe',
|
|
584
|
+
});
|
|
585
|
+
if (result.error) {
|
|
586
|
+
return { ok: false, message: result.error.message };
|
|
587
|
+
}
|
|
588
|
+
if (result.status !== 0) {
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
message: result.stderr.trim() || result.stdout.trim() || `Exited with ${result.status}`,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return { ok: true, message: `Ran ${formatCommand(command)}` };
|
|
595
|
+
}
|
|
596
|
+
function formatCommand(command) {
|
|
597
|
+
return command.map(shellQuote).join(' ');
|
|
598
|
+
}
|
|
599
|
+
function shellQuote(value) {
|
|
600
|
+
return /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : JSON.stringify(value);
|
|
601
|
+
}
|
|
602
|
+
function defaultTimestamp() {
|
|
603
|
+
return new Date()
|
|
604
|
+
.toISOString()
|
|
605
|
+
.replace(/[-:]/g, '')
|
|
606
|
+
.replace(/\.\d{3}Z$/, 'Z');
|
|
607
|
+
}
|
|
608
|
+
function ensureTrailingNewline(value) {
|
|
609
|
+
return value.endsWith('\n') ? value : `${value}\n`;
|
|
610
|
+
}
|
|
611
|
+
function isRecord(value) {
|
|
612
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
613
|
+
}
|
|
614
|
+
function readFlagValue(argv, index, flag) {
|
|
615
|
+
const value = argv[index + 1];
|
|
616
|
+
if (!value || value.startsWith('--')) {
|
|
617
|
+
throw new Error(`Missing value for ${flag}`);
|
|
618
|
+
}
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
621
|
+
function readInlineFlagValue(arg) {
|
|
622
|
+
const value = arg.slice(arg.indexOf('=') + 1);
|
|
623
|
+
if (!value) {
|
|
624
|
+
throw new Error(`Missing value for ${arg.slice(0, arg.indexOf('='))}`);
|
|
625
|
+
}
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
function parseAccess(value) {
|
|
629
|
+
if (value === 'full' || value === 'docs')
|
|
630
|
+
return value;
|
|
631
|
+
throw new Error(`Invalid --access value: ${value}`);
|
|
632
|
+
}
|
|
633
|
+
function parseAuth(value) {
|
|
634
|
+
if (value === 'oauth' || value === 'api-key')
|
|
635
|
+
return value;
|
|
636
|
+
throw new Error(`Invalid --auth value: ${value}`);
|
|
637
|
+
}
|
|
638
|
+
function parseScope(value) {
|
|
639
|
+
if (value === 'user' || value === 'project')
|
|
640
|
+
return value;
|
|
641
|
+
throw new Error(`Invalid --scope value: ${value}`);
|
|
642
|
+
}
|
|
643
|
+
function parseTools(value) {
|
|
644
|
+
const tools = value
|
|
645
|
+
.split(',')
|
|
646
|
+
.map((item) => normalizeTool(item.trim()))
|
|
647
|
+
.filter((item) => Boolean(item));
|
|
648
|
+
if (tools.length === 0) {
|
|
649
|
+
throw new Error('At least one tool is required.');
|
|
650
|
+
}
|
|
651
|
+
return Array.from(new Set(tools));
|
|
652
|
+
}
|
|
653
|
+
function normalizeTool(value) {
|
|
654
|
+
if (!value)
|
|
655
|
+
return undefined;
|
|
656
|
+
if (value === 'claude-code' || value === 'claude_code')
|
|
657
|
+
return 'claude';
|
|
658
|
+
if (TOOL_LABELS.has(value))
|
|
659
|
+
return value;
|
|
660
|
+
throw new Error(`Invalid tool: ${value}`);
|
|
661
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spekoai/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Local stdio bridge for the hosted SpekoAI MCP server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Speko",
|
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"provenance": true
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@clack/prompts": "^1.4.0",
|
|
51
|
+
"jsonc-parser": "^3.3.1",
|
|
50
52
|
"mcp-remote": "^0.1.38",
|
|
53
|
+
"smol-toml": "^1.6.1",
|
|
51
54
|
"tslib": "^2.3.0"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|