@extuitive/skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +514 -0
- package/bin/cli.mjs +932 -0
- package/package.json +40 -0
- package/skills/extuitive/SKILL.md +63 -0
- package/skills/extuitive/references/connect.md +75 -0
- package/skills/extuitive/references/init.md +83 -0
- package/skills/extuitive/references/select.md +89 -0
- package/skills/extuitive/references/tools.md +252 -0
- package/skills/extuitive/references/upload-status.md +102 -0
- package/skills/extuitive/references/upload.md +160 -0
- package/skills/extuitive/scripts/upload.mjs +340 -0
- package/src/constants.mjs +63 -0
- package/src/doctor.mjs +513 -0
- package/src/exec.mjs +139 -0
- package/src/hosts.mjs +350 -0
- package/src/install.mjs +541 -0
- package/src/mcp-setup.mjs +476 -0
- package/src/zip.mjs +246 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that knows how each host is told about an MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Every command string for both hosts lives here. Install runs them, `doctor` prints them
|
|
5
|
+
* when something is missing, and the skills reference neither — they tell the agent to run
|
|
6
|
+
* `<npx> doctor` and relay what it says. That indirection exists because these
|
|
7
|
+
* strings are the most likely thing in the package to go stale: they belong to Claude Code
|
|
8
|
+
* and Codex, not to us, and a skill reciting a command that no longer exists is worse than
|
|
9
|
+
* a skill that stays quiet. Keeping them in one module means a host CLI change is a fix
|
|
10
|
+
* here and nowhere else.
|
|
11
|
+
*
|
|
12
|
+
* Registration deliberately shells out to each host's own CLI rather than editing its
|
|
13
|
+
* config file. The file formats differ (JSON for Claude Code, TOML for Codex), both have
|
|
14
|
+
* changed shape across versions, and a hand-written entry that a newer host rejects is
|
|
15
|
+
* harder to diagnose than a command that failed loudly. Hand-editing is the fallback only
|
|
16
|
+
* when the CLI is absent or broken, and then it is printed for the user rather than applied.
|
|
17
|
+
*
|
|
18
|
+
* Commands spawn `host.cliCommand`, which is whichever binary `resolveCli` found working —
|
|
19
|
+
* possibly the one inside the desktop app bundle — and print the same, so a step someone is
|
|
20
|
+
* told to run is the step that will actually run.
|
|
21
|
+
*
|
|
22
|
+
* Claude Desktop has neither a CLI nor a config file we may write, so everything below
|
|
23
|
+
* takes a second path for it, chosen by `mcpSetup` rather than by host id. See
|
|
24
|
+
* `connectorSteps` for why its config file is left alone even as a fallback.
|
|
25
|
+
*/
|
|
26
|
+
import { DEFAULT_MCP_ENDPOINT, MCP_SERVER_NAME, NPX_COMMAND } from "./constants.mjs";
|
|
27
|
+
import { formatCommand, run } from "./exec.mjs";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The command that registers the server.
|
|
31
|
+
*
|
|
32
|
+
* `--scope user` on Claude Code is not a preference. The default is `local`, which binds the
|
|
33
|
+
* server to whichever directory the command ran in, while skills installed to
|
|
34
|
+
* `~/.claude/skills` are available everywhere — so the default would produce a setup that
|
|
35
|
+
* works in one project and looks broken in every other one.
|
|
36
|
+
*/
|
|
37
|
+
export function registerCommand(host, { endpoint = DEFAULT_MCP_ENDPOINT, scope = "user" } = {}) {
|
|
38
|
+
if (host.mcpSetup !== "cli") {
|
|
39
|
+
throw new Error(`${host.label} has no CLI to register with; use connectorSteps instead.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (host.id === "claude") {
|
|
43
|
+
const args = ["mcp", "add", "--transport", "http", MCP_SERVER_NAME, endpoint];
|
|
44
|
+
if (scope === "project") {
|
|
45
|
+
args.push("--scope", "project");
|
|
46
|
+
} else {
|
|
47
|
+
args.push("--scope", "user");
|
|
48
|
+
}
|
|
49
|
+
return { command: host.cliCommand, args };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (host.id === "codex") {
|
|
53
|
+
return {
|
|
54
|
+
command: host.cliCommand,
|
|
55
|
+
args: ["mcp", "add", MCP_SERVER_NAME, "--url", endpoint],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(`Unknown host: ${host.id}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The command that takes the server back out.
|
|
64
|
+
*
|
|
65
|
+
* Claude Code needs the scope repeated. Removal looks in one scope at a time and defaults to
|
|
66
|
+
* `local`, so a remove without `--scope user` reports success having deleted nothing — the
|
|
67
|
+
* user-scoped entry install wrote is still there. Codex keeps a single list and takes only a
|
|
68
|
+
* name.
|
|
69
|
+
*/
|
|
70
|
+
export function unregisterCommand(host, { scope = "user" } = {}) {
|
|
71
|
+
if (host.mcpSetup !== "cli") {
|
|
72
|
+
throw new Error(`${host.label} has no CLI to unregister with; use connectorSteps instead.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (host.id === "claude") {
|
|
76
|
+
return {
|
|
77
|
+
command: host.cliCommand,
|
|
78
|
+
args: ["mcp", "remove", MCP_SERVER_NAME, "--scope", scope === "project" ? "project" : "user"],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (host.id === "codex") {
|
|
83
|
+
return { command: host.cliCommand, args: ["mcp", "remove", MCP_SERVER_NAME] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Unknown host: ${host.id}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The panel a person adds the server through, for a host with no CLI.
|
|
91
|
+
*
|
|
92
|
+
* This is the whole of Claude Desktop's setup, and it is deliberately not backed by a config
|
|
93
|
+
* file fallback the way the CLI hosts are. `claude_desktop_config.json` validates stdio
|
|
94
|
+
* servers only: an entry carrying a `url` is not ignored but *destructive* — Claude Desktop
|
|
95
|
+
* rewrites the file on next launch with the entire `mcpServers` block stripped out, taking
|
|
96
|
+
* any servers the person added by hand with it. There is a way to reach an HTTPS endpoint
|
|
97
|
+
* from that file, by spawning `npx mcp-remote` as a stdio bridge, and it is left out on
|
|
98
|
+
* purpose: it puts a second OAuth implementation and a background Node process between the
|
|
99
|
+
* app and a server the app can already talk to directly.
|
|
100
|
+
*
|
|
101
|
+
* Written as steps rather than prose because the caller prints them numbered, and because a
|
|
102
|
+
* connector added at the wrong level — the personal panel versus the organisation one — is
|
|
103
|
+
* a failure that looks like the URL being wrong.
|
|
104
|
+
*/
|
|
105
|
+
export function connectorSteps(host, { endpoint = DEFAULT_MCP_ENDPOINT } = {}) {
|
|
106
|
+
return [
|
|
107
|
+
"Open Settings, then Connectors.",
|
|
108
|
+
"Click Add custom connector.",
|
|
109
|
+
`Paste this as the remote MCP server URL: ${endpoint}`,
|
|
110
|
+
"Click Add. Claude checks the URL and fills in the authentication settings it finds.",
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How a person completes OAuth.
|
|
116
|
+
*
|
|
117
|
+
* Never run for them. The flow opens a browser and finishes against their own session, so
|
|
118
|
+
* the honest thing is to name the step and stop.
|
|
119
|
+
*
|
|
120
|
+
* Claude Code gets `/mcp` and nothing else. `claude mcp login extuitive` is a real command
|
|
121
|
+
* on new enough versions, but offering it here was a bug: what reads this output is usually
|
|
122
|
+
* an agent inside a Claude Code session, and the one thing it can do with a shell command is
|
|
123
|
+
* run it — in bash mode, in a session that has no `extuitive` server to log into, on a
|
|
124
|
+
* version that may not have the subcommand at all. Every one of those fails in a way that
|
|
125
|
+
* looks like the install failed. `/mcp` is the person's own panel and cannot be run by
|
|
126
|
+
* mistake on their behalf.
|
|
127
|
+
*
|
|
128
|
+
* `inSession` says where the step happens, which decides the order of everything around it.
|
|
129
|
+
* Claude Code signs in from inside a session and so cannot do it before restarting; Codex
|
|
130
|
+
* signs in from a terminal and so can do it whenever.
|
|
131
|
+
*/
|
|
132
|
+
export function authInstructions(host) {
|
|
133
|
+
if (host.id === "claude") {
|
|
134
|
+
return {
|
|
135
|
+
primary: "Run /mcp, choose extuitive, and approve access.",
|
|
136
|
+
alternative: null,
|
|
137
|
+
inSession: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (host.id === "codex") {
|
|
142
|
+
return {
|
|
143
|
+
// The desktop app has its own button for this — Settings > MCP servers, then
|
|
144
|
+
// Authenticate — but the command is what gets printed, because it works from every
|
|
145
|
+
// Codex surface including the app, and because one instruction that always applies
|
|
146
|
+
// beats two that each apply sometimes.
|
|
147
|
+
primary: formatCommand(host.cliCommand, ["mcp", "login", MCP_SERVER_NAME]),
|
|
148
|
+
alternative: "In the Codex desktop app: Settings > MCP servers > Authenticate.",
|
|
149
|
+
inSession: false,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (host.id === "claude-desktop") {
|
|
154
|
+
return {
|
|
155
|
+
// Adding the connector and signing in are one continuous flow here — the browser
|
|
156
|
+
// opens off the back of the Add, or off Connect if it was dismissed. Naming the
|
|
157
|
+
// second button matters for the person who closed the window and now sees a
|
|
158
|
+
// connector sitting there doing nothing.
|
|
159
|
+
primary: "Approve access in the browser window that opens after you add the connector.",
|
|
160
|
+
alternative: "If you closed it, click Connect next to extuitive in Settings > Connectors.",
|
|
161
|
+
inSession: false,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(`Unknown host: ${host.id}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* When the skill itself becomes usable, in the host's own terms.
|
|
170
|
+
*
|
|
171
|
+
* Every host now picks up new skills without a restart — Codex between turns, Claude Code as
|
|
172
|
+
* files change, Claude Desktop when a chat starts — so this is one sentence and not an
|
|
173
|
+
* instruction. It exists as a function because it is the sentence an installing agent will
|
|
174
|
+
* repeat to the person, and it must not drift from what the host does: telling someone to
|
|
175
|
+
* restart for a skill that is already live is how they learn to ignore the rest of the
|
|
176
|
+
* output.
|
|
177
|
+
*/
|
|
178
|
+
export function skillAvailabilityNotice(host) {
|
|
179
|
+
if (host.skillDelivery === "bundle") {
|
|
180
|
+
// Nothing is live yet. The archive is on disk and the skill reaches the account only
|
|
181
|
+
// once someone uploads it, so the sentence names the upload rather than a wait.
|
|
182
|
+
return `Once uploaded, the skill is available in new ${host.sessionNoun}s.`;
|
|
183
|
+
}
|
|
184
|
+
if (host.loadsSkillsAtStartup === true) {
|
|
185
|
+
return `${host.label} reads skills once at startup, so the skill appears after you start a new ${host.sessionNoun}.`;
|
|
186
|
+
}
|
|
187
|
+
if (host.id === "codex") {
|
|
188
|
+
return "The skill is available on your next turn.";
|
|
189
|
+
}
|
|
190
|
+
return "The skill is available now.";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* When the MCP server — and so the Extuitive tools — becomes usable.
|
|
195
|
+
*
|
|
196
|
+
* Stated as its own sentence everywhere it appears because it is the step people skip and
|
|
197
|
+
* then report as a broken install: the server is registered, the endpoint is fine, `mcp list`
|
|
198
|
+
* agrees, and the session still has no Extuitive tools. On Claude Code it also gates sign-in,
|
|
199
|
+
* so it has to be said before the sign-in step rather than after it.
|
|
200
|
+
*/
|
|
201
|
+
export function serverAvailabilityNotice(host) {
|
|
202
|
+
if (host.id === "claude-desktop") {
|
|
203
|
+
// No application restart: a connector and an uploaded skill are both live as soon as
|
|
204
|
+
// they land. The chat is the thing with the stale view, and saying "restart Claude
|
|
205
|
+
// Desktop" would send someone quitting an app that did not need it and still landing
|
|
206
|
+
// back in the same conversation.
|
|
207
|
+
return `${host.label} gives a chat its tools and skills when the chat starts, so neither appears in a conversation that was already open — start a new chat.`;
|
|
208
|
+
}
|
|
209
|
+
if (host.id === "claude") {
|
|
210
|
+
return `${host.label} connects MCP servers when a session starts, so extuitive is not in the session you ran this from — and /mcp cannot sign in to a server that session never connected to.`;
|
|
211
|
+
}
|
|
212
|
+
return `${host.label} connects MCP servers when a session starts, so the Extuitive tools appear in a new session once you have signed in.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The command that reports per-server health, which `doctor` prefers over its own probing.
|
|
217
|
+
*
|
|
218
|
+
* `null` for a host that has no CLI. Doctor reports that as "cannot be checked from here"
|
|
219
|
+
* rather than as a problem, because the alternative — inferring from our own probe — would
|
|
220
|
+
* report every unsigned-in server as missing and every missing one as unsigned-in.
|
|
221
|
+
*/
|
|
222
|
+
export function statusCommand(host) {
|
|
223
|
+
if (host.mcpSetup !== "cli") {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return { command: host.cliCommand, args: ["mcp", "list"] };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* What to paste when the host CLI cannot be run and we will not guess at its config.
|
|
231
|
+
*
|
|
232
|
+
* `null` for a connector-UI host. Not because there is no file — there is one, right where
|
|
233
|
+
* you would expect — but because writing a remote server into it makes Claude Desktop
|
|
234
|
+
* delete the whole `mcpServers` block on next launch. Printing a snippet somebody could
|
|
235
|
+
* paste there would be handing them the destructive version of the thing they asked for.
|
|
236
|
+
*/
|
|
237
|
+
export function manualConfigSnippet(host, { endpoint = DEFAULT_MCP_ENDPOINT } = {}) {
|
|
238
|
+
if (host.mcpSetup !== "cli") {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (host.id === "codex") {
|
|
243
|
+
return {
|
|
244
|
+
path: host.configPath,
|
|
245
|
+
language: "toml",
|
|
246
|
+
body: `[mcp_servers.${MCP_SERVER_NAME}]\nurl = "${endpoint}"\n`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
path: ".mcp.json (project) or ~/.claude.json (user)",
|
|
252
|
+
language: "json",
|
|
253
|
+
body: `${JSON.stringify(
|
|
254
|
+
{
|
|
255
|
+
mcpServers: {
|
|
256
|
+
[MCP_SERVER_NAME]: { type: "http", url: endpoint },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
null,
|
|
260
|
+
2,
|
|
261
|
+
)}\n`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Register the server, or explain how to.
|
|
267
|
+
*
|
|
268
|
+
* A failure here is reported, not thrown. Skills are already on disk by this point and are
|
|
269
|
+
* useful the moment the server is registered by any means, so aborting the whole install
|
|
270
|
+
* over a CLI that refused would leave a worse state than finishing and printing the step.
|
|
271
|
+
*
|
|
272
|
+
* `cli_missing` and `cli_broken` are different statuses because they have different fixes:
|
|
273
|
+
* one person has to install the CLI, the other has one that does not run and should be told
|
|
274
|
+
* which file it is.
|
|
275
|
+
*/
|
|
276
|
+
export async function registerMcpServer(host, options = {}) {
|
|
277
|
+
const { endpoint = DEFAULT_MCP_ENDPOINT, scope = "user", dryRun = false, cliAvailable } = options;
|
|
278
|
+
|
|
279
|
+
// Not a degraded outcome and not a failure — it is how this host is set up, every time.
|
|
280
|
+
// Kept distinct from `cli_missing` so callers can say "here is what to click" instead of
|
|
281
|
+
// "something went wrong, here is what to click".
|
|
282
|
+
if (host.mcpSetup === "connector-ui") {
|
|
283
|
+
return { status: "manual_only", steps: connectorSteps(host, { endpoint }) };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { command, args } = registerCommand(host, { endpoint, scope });
|
|
287
|
+
const rendered = formatCommand(command, args);
|
|
288
|
+
|
|
289
|
+
if (cliAvailable === false) {
|
|
290
|
+
return {
|
|
291
|
+
status: host.cliResolution.state === "broken" ? "cli_broken" : "cli_missing",
|
|
292
|
+
command: rendered,
|
|
293
|
+
detail: host.cliResolution.detail,
|
|
294
|
+
manual: manualConfigSnippet(host, { endpoint }),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (dryRun === true) {
|
|
299
|
+
return { status: "skipped_dry_run", command: rendered };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const timeoutMs = 30_000;
|
|
303
|
+
const result = run(command, args, { timeoutMs });
|
|
304
|
+
if (result.ok === true) {
|
|
305
|
+
return { status: "registered", command: rendered };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (result.timedOut === true) {
|
|
309
|
+
return {
|
|
310
|
+
status: "failed",
|
|
311
|
+
command: rendered,
|
|
312
|
+
detail:
|
|
313
|
+
`${host.cli} did not finish within ${timeoutMs / 1000}s and was stopped. It may be waiting ` +
|
|
314
|
+
`for input, which it cannot receive here. Run it yourself in a terminal: ${rendered}`,
|
|
315
|
+
manual: manualConfigSnippet(host, { endpoint }),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Adding a server that is already configured is a refusal, not a problem: the desired end
|
|
320
|
+
// state is the one we already have. Detected by message because neither CLI gives it a
|
|
321
|
+
// distinct exit code.
|
|
322
|
+
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
323
|
+
if (output.includes("already exists") === true || output.includes("already configured") === true) {
|
|
324
|
+
return { status: "already_registered", command: rendered };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
status: "failed",
|
|
329
|
+
command: rendered,
|
|
330
|
+
detail: (result.stderr || result.stdout).trim(),
|
|
331
|
+
manual: manualConfigSnippet(host, { endpoint }),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Unregister the server, or explain how to.
|
|
337
|
+
*
|
|
338
|
+
* Reports rather than throws, for the same reason registration does: by the time this runs
|
|
339
|
+
* the skills are already gone, and aborting on a CLI that refused would leave a half-removed
|
|
340
|
+
* setup with no message about which half.
|
|
341
|
+
*/
|
|
342
|
+
export async function unregisterMcpServer(host, options = {}) {
|
|
343
|
+
const { scope = "user", dryRun = false, cliAvailable } = options;
|
|
344
|
+
|
|
345
|
+
if (host.mcpSetup === "connector-ui") {
|
|
346
|
+
return {
|
|
347
|
+
status: "manual_only",
|
|
348
|
+
steps: [
|
|
349
|
+
"Open Settings, then Connectors.",
|
|
350
|
+
`Find ${MCP_SERVER_NAME} and remove it.`,
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { command, args } = unregisterCommand(host, { scope });
|
|
356
|
+
const rendered = formatCommand(command, args);
|
|
357
|
+
|
|
358
|
+
if (cliAvailable === false) {
|
|
359
|
+
return {
|
|
360
|
+
status: host.cliResolution.state === "broken" ? "cli_broken" : "cli_missing",
|
|
361
|
+
command: rendered,
|
|
362
|
+
detail: host.cliResolution.detail,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (dryRun === true) {
|
|
367
|
+
return { status: "skipped_dry_run", command: rendered };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const timeoutMs = 30_000;
|
|
371
|
+
const result = run(command, args, { timeoutMs });
|
|
372
|
+
|
|
373
|
+
if (result.timedOut === true) {
|
|
374
|
+
return {
|
|
375
|
+
status: "failed",
|
|
376
|
+
command: rendered,
|
|
377
|
+
detail:
|
|
378
|
+
`${host.cli} did not finish within ${timeoutMs / 1000}s and was stopped. Run it yourself ` +
|
|
379
|
+
`in a terminal: ${rendered}`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Checked before the exit status, not after, because removing a server that was never
|
|
384
|
+
// there is a *success* for Codex: `codex mcp remove missing` prints "No MCP server named
|
|
385
|
+
// 'missing' found." and exits 0. Reading only the exit code would report having removed
|
|
386
|
+
// something that was not there, which is the one thing an uninstall must not invent.
|
|
387
|
+
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
388
|
+
const absent = ["not found", "no such", "does not exist", "no mcp server"].some((phrase) =>
|
|
389
|
+
output.includes(phrase),
|
|
390
|
+
);
|
|
391
|
+
if (absent === true) {
|
|
392
|
+
return { status: "already_absent", command: rendered };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (result.ok === true) {
|
|
396
|
+
return { status: "unregistered", command: rendered };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
status: "failed",
|
|
401
|
+
command: rendered,
|
|
402
|
+
detail: (result.stderr || result.stdout).trim(),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The literal lines a user needs when nothing can be done for them automatically.
|
|
408
|
+
*
|
|
409
|
+
* `bundle` is the path to an archive a bundle-delivery host expects to be uploaded. Passed
|
|
410
|
+
* in rather than derived, because where it was written is the caller's decision — `--dir`
|
|
411
|
+
* moves it — and a list of steps naming a file that is somewhere else is worse than no list.
|
|
412
|
+
*/
|
|
413
|
+
export function manualSteps(
|
|
414
|
+
host,
|
|
415
|
+
{ endpoint = DEFAULT_MCP_ENDPOINT, scope = "user", bundle = null } = {},
|
|
416
|
+
) {
|
|
417
|
+
const steps = [];
|
|
418
|
+
|
|
419
|
+
if (host.skillDelivery === "bundle" && bundle !== null) {
|
|
420
|
+
steps.push({
|
|
421
|
+
title: "Upload the skill",
|
|
422
|
+
body: `Settings > Capabilities: turn on code execution and file creation.\nCustomize > Skills: click +, then Create skill, then Upload a skill.\nChoose: ${bundle}`,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (host.mcpSetup === "connector-ui") {
|
|
427
|
+
steps.push({
|
|
428
|
+
title: "Add the connector",
|
|
429
|
+
body: connectorSteps(host, { endpoint })
|
|
430
|
+
.map((step, index) => `${index + 1}. ${step}`)
|
|
431
|
+
.join("\n"),
|
|
432
|
+
});
|
|
433
|
+
} else {
|
|
434
|
+
const { command, args } = registerCommand(host, { endpoint, scope });
|
|
435
|
+
const snippet = manualConfigSnippet(host, { endpoint });
|
|
436
|
+
steps.push({
|
|
437
|
+
title: "Register the MCP server",
|
|
438
|
+
body: `${formatCommand(command, args)}\nor add to ${snippet.path}:\n${snippet.body.trimEnd()}`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The restart and the sign-in are ordered by where the sign-in happens. On Claude Code it
|
|
443
|
+
// happens inside a session, and `/mcp` offers only servers that session connected to at
|
|
444
|
+
// startup — so a list that signs in first and restarts second describes something nobody
|
|
445
|
+
// can do. Codex signs in from a terminal and opens a new session afterwards for the tools.
|
|
446
|
+
const auth = authInstructions(host);
|
|
447
|
+
const restart = {
|
|
448
|
+
title: `Start a new ${host.label} ${host.sessionNoun}`,
|
|
449
|
+
body: serverAvailabilityNotice(host),
|
|
450
|
+
};
|
|
451
|
+
const signIn = {
|
|
452
|
+
title: "Sign in",
|
|
453
|
+
body: auth.alternative === null ? auth.primary : `${auth.primary}\n${auth.alternative}`,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
steps.push(...(auth.inSession === true ? [restart, signIn] : [signIn, restart]));
|
|
457
|
+
|
|
458
|
+
steps.push({
|
|
459
|
+
title: "Check it worked",
|
|
460
|
+
body: `${NPX_COMMAND} doctor`,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// The prefix is not cosmetic. Codex reserves `/` for its own commands and answers an
|
|
464
|
+
// unknown one with "Unrecognized command", which looks exactly like the skill failing
|
|
465
|
+
// to install, so the working syntax has to be stated rather than inferred. Where there is
|
|
466
|
+
// no prefix at all, saying which one to type would be worse than saying nothing.
|
|
467
|
+
steps.push({
|
|
468
|
+
title: `Use it in ${host.label}`,
|
|
469
|
+
body:
|
|
470
|
+
host.invocationNote === null
|
|
471
|
+
? `${host.invocationPrefix}extuitive init`
|
|
472
|
+
: host.invocationNote,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return steps;
|
|
476
|
+
}
|