@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 CHANGED
@@ -1,128 +1,42 @@
1
1
  # @spekoai/mcp
2
2
 
3
- Local stdio bridge for MCP clients that cannot connect to remote HTTP MCP
4
- servers directly. It proxies SpekoAI's hosted MCP server and does not contain
5
- Speko tool logic of its own.
6
-
7
- Use the hosted endpoint directly when your client supports remote MCP:
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 --help
12
+ npx @spekoai/mcp@latest init
25
13
  ```
26
14
 
27
- The package exposes the `spekoai-mcp` binary.
28
-
29
- ## Claude Code
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 a stdio bridge install:
19
+ For scripted setup, pass the choices explicitly:
38
20
 
39
21
  ```bash
40
- claude mcp add spekoai -- npx -y @spekoai/mcp@latest
22
+ npx @spekoai/mcp@latest init --access full --auth oauth --tools claude,codex --scope user --yes
41
23
  ```
42
24
 
43
- For API-key auth in a headless setup, provide `SPEKO_API_KEY` in the MCP
44
- client environment. The bridge forwards it as `Authorization: Bearer ...`.
45
-
46
- ## Cursor
25
+ ## Bridge
47
26
 
48
- Add this to `~/.cursor/mcp.json`:
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
- With API-key auth:
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
- ## OpenCode
78
-
79
- Add this to `opencode.json`:
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
- With API-key auth:
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 `mcp-remote`.
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
- For `mcp-remote` auth cache issues, restart the MCP client after clearing:
159
-
160
- ```bash
161
- rm -rf ~/.mcp-auth
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"}
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_AUTH_MCP_URL = 'https://mcp.speko.ai/mcp-auth';
2
+ export const DEFAULT_PUBLIC_MCP_URL = 'https://mcp.speko.ai/mcp';
3
+ export const AUTH_HEADER_ENV = 'SPEKOAI_MCP_AUTH_HEADER';
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- export declare const DEFAULT_AUTH_MCP_URL = "https://mcp.speko.ai/mcp-auth";
3
- export declare const DEFAULT_PUBLIC_MCP_URL = "https://mcp.speko.ai/mcp";
4
- export declare const AUTH_HEADER_ENV = "SPEKOAI_MCP_AUTH_HEADER";
5
- export type Environment = Record<string, string | undefined>;
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 [url] [mcp-remote flags]\n\nBridge local stdio MCP clients to SpekoAI's hosted MCP server.\n\nDefaults:\n Authenticated endpoint: https://mcp.speko.ai/mcp-auth\n Public-only endpoint: 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 SPEKOAI_MCP_AUTH_URL Override the authenticated endpoint.\n SPEKOAI_MCP_URL Override the public endpoint.\n SPEKO_API_KEY Forward as Authorization bearer token.\n\nExamples:\n spekoai-mcp\n spekoai-mcp --public\n SPEKO_API_KEY=sk_live_xxx spekoai-mcp\n SPEKOAI_MCP_AUTH_URL=https://mcp-staging.speko.dev/mcp-auth spekoai-mcp\n spekoai-mcp https://mcp-staging.speko.dev/mcp-auth --debug\n\nAll remaining arguments are passed through to mcp-remote.\n";
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAKA,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;AAE7D,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,g1BAyBrB,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,wBAAsB,GAAG,CAAC,IAAI,WAAwB,EAAE,GAAG,oBAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBxF"}
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
- export const DEFAULT_AUTH_MCP_URL = 'https://mcp.speko.ai/mcp-auth';
5
- export const DEFAULT_PUBLIC_MCP_URL = 'https://mcp.speko.ai/mcp';
6
- export const AUTH_HEADER_ENV = 'SPEKOAI_MCP_AUTH_HEADER';
7
- export const HELP_TEXT = `Usage: spekoai-mcp [url] [mcp-remote flags]
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
- Bridge local stdio MCP clients to SpekoAI's hosted MCP server.
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
- Authenticated endpoint: ${DEFAULT_AUTH_MCP_URL}
13
- Public-only endpoint: ${DEFAULT_PUBLIC_MCP_URL}
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 Connect to the public docs/scaffolding endpoint.
17
- -h, --help Print this help text.
35
+ --public Connect to the public docs/scaffolding endpoint.
36
+ -h, --help Print this help text.
18
37
 
19
38
  Environment:
20
- SPEKOAI_MCP_AUTH_URL Override the authenticated endpoint.
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(HELP_TEXT);
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 (pathToFileURL(process.argv[1] ?? '').href === import.meta.url) {
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.4",
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": {