@tabai/sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +401 -0
- package/bin/tab.mjs +23 -0
- package/dist/_shared/abi.d.ts +150 -0
- package/dist/_shared/abi.d.ts.map +1 -0
- package/dist/_shared/abi.js +197 -0
- package/dist/_shared/abi.js.map +1 -0
- package/dist/_shared/chains.d.ts +118 -0
- package/dist/_shared/chains.d.ts.map +1 -0
- package/dist/_shared/chains.js +89 -0
- package/dist/_shared/chains.js.map +1 -0
- package/dist/_shared/hex.d.ts +35 -0
- package/dist/_shared/hex.d.ts.map +1 -0
- package/dist/_shared/hex.js +40 -0
- package/dist/_shared/hex.js.map +1 -0
- package/dist/_shared/index.d.ts +14 -0
- package/dist/_shared/index.d.ts.map +1 -0
- package/dist/_shared/index.js +14 -0
- package/dist/_shared/index.js.map +1 -0
- package/dist/_shared/keccak256.d.ts +29 -0
- package/dist/_shared/keccak256.d.ts.map +1 -0
- package/dist/_shared/keccak256.js +145 -0
- package/dist/_shared/keccak256.js.map +1 -0
- package/dist/_shared/result.d.ts +78 -0
- package/dist/_shared/result.d.ts.map +1 -0
- package/dist/_shared/result.js +61 -0
- package/dist/_shared/result.js.map +1 -0
- package/dist/cli/client-config.d.ts +155 -0
- package/dist/cli/client-config.d.ts.map +1 -0
- package/dist/cli/client-config.js +382 -0
- package/dist/cli/client-config.js.map +1 -0
- package/dist/cli/connect.d.ts +76 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +158 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/doctor.d.ts +57 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +253 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +45 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +371 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/http/client-402.d.ts +243 -0
- package/dist/http/client-402.d.ts.map +1 -0
- package/dist/http/client-402.js +515 -0
- package/dist/http/client-402.js.map +1 -0
- package/dist/http/headers.d.ts +173 -0
- package/dist/http/headers.d.ts.map +1 -0
- package/dist/http/headers.js +284 -0
- package/dist/http/headers.js.map +1 -0
- package/dist/http/index.d.ts +15 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +15 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/metering-claim.d.ts +82 -0
- package/dist/http/metering-claim.d.ts.map +1 -0
- package/dist/http/metering-claim.js +99 -0
- package/dist/http/metering-claim.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +40 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +50 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/assets.d.ts +31 -0
- package/dist/mcp/assets.d.ts.map +1 -0
- package/dist/mcp/assets.js +78 -0
- package/dist/mcp/assets.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/json-schema.d.ts +86 -0
- package/dist/mcp/json-schema.d.ts.map +1 -0
- package/dist/mcp/json-schema.js +215 -0
- package/dist/mcp/json-schema.js.map +1 -0
- package/dist/mcp/json.d.ts +43 -0
- package/dist/mcp/json.d.ts.map +1 -0
- package/dist/mcp/json.js +69 -0
- package/dist/mcp/json.js.map +1 -0
- package/dist/mcp/registry-client.d.ts +88 -0
- package/dist/mcp/registry-client.d.ts.map +1 -0
- package/dist/mcp/registry-client.js +158 -0
- package/dist/mcp/registry-client.js.map +1 -0
- package/dist/mcp/schemas.d.ts +82 -0
- package/dist/mcp/schemas.d.ts.map +1 -0
- package/dist/mcp/schemas.js +493 -0
- package/dist/mcp/schemas.js.map +1 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +285 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/settings.d.ts +90 -0
- package/dist/mcp/settings.d.ts.map +1 -0
- package/dist/mcp/settings.js +160 -0
- package/dist/mcp/settings.js.map +1 -0
- package/dist/mcp/toolset.d.ts +231 -0
- package/dist/mcp/toolset.d.ts.map +1 -0
- package/dist/mcp/toolset.js +760 -0
- package/dist/mcp/toolset.js.map +1 -0
- package/dist/payments/abi.d.ts +9 -0
- package/dist/payments/abi.d.ts.map +1 -0
- package/dist/payments/abi.js +17 -0
- package/dist/payments/abi.js.map +1 -0
- package/dist/payments/config.d.ts +199 -0
- package/dist/payments/config.d.ts.map +1 -0
- package/dist/payments/config.js +259 -0
- package/dist/payments/config.js.map +1 -0
- package/dist/payments/index.d.ts +13 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +13 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/kuru.d.ts +191 -0
- package/dist/payments/kuru.d.ts.map +1 -0
- package/dist/payments/kuru.js +377 -0
- package/dist/payments/kuru.js.map +1 -0
- package/dist/payments/monad.d.ts +69 -0
- package/dist/payments/monad.d.ts.map +1 -0
- package/dist/payments/monad.js +306 -0
- package/dist/payments/monad.js.map +1 -0
- package/dist/payments/permit2.d.ts +118 -0
- package/dist/payments/permit2.d.ts.map +1 -0
- package/dist/payments/permit2.js +366 -0
- package/dist/payments/permit2.js.map +1 -0
- package/dist/payments/registry.d.ts +119 -0
- package/dist/payments/registry.d.ts.map +1 -0
- package/dist/payments/registry.js +199 -0
- package/dist/payments/registry.js.map +1 -0
- package/dist/payments/strategy.d.ts +80 -0
- package/dist/payments/strategy.d.ts.map +1 -0
- package/dist/payments/strategy.js +103 -0
- package/dist/payments/strategy.js.map +1 -0
- package/dist/proxy/hooks.d.ts +90 -0
- package/dist/proxy/hooks.d.ts.map +1 -0
- package/dist/proxy/hooks.js +35 -0
- package/dist/proxy/hooks.js.map +1 -0
- package/dist/proxy/index.d.ts +9 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +9 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/proxy.d.ts +156 -0
- package/dist/proxy/proxy.d.ts.map +1 -0
- package/dist/proxy/proxy.js +366 -0
- package/dist/proxy/proxy.js.map +1 -0
- package/dist/server/adapters/express.d.ts +89 -0
- package/dist/server/adapters/express.d.ts.map +1 -0
- package/dist/server/adapters/express.js +215 -0
- package/dist/server/adapters/express.js.map +1 -0
- package/dist/server/adapters/hono.d.ts +52 -0
- package/dist/server/adapters/hono.d.ts.map +1 -0
- package/dist/server/adapters/hono.js +61 -0
- package/dist/server/adapters/hono.js.map +1 -0
- package/dist/server/adapters/next.d.ts +52 -0
- package/dist/server/adapters/next.d.ts.map +1 -0
- package/dist/server/adapters/next.js +56 -0
- package/dist/server/adapters/next.js.map +1 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +30 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/metering.d.ts +209 -0
- package/dist/server/metering.d.ts.map +1 -0
- package/dist/server/metering.js +365 -0
- package/dist/server/metering.js.map +1 -0
- package/dist/server/post-paid.d.ts +355 -0
- package/dist/server/post-paid.d.ts.map +1 -0
- package/dist/server/post-paid.js +512 -0
- package/dist/server/post-paid.js.map +1 -0
- package/dist/x402/client.d.ts +203 -0
- package/dist/x402/client.d.ts.map +1 -0
- package/dist/x402/client.js +337 -0
- package/dist/x402/client.js.map +1 -0
- package/dist/x402/hub.d.ts +79 -0
- package/dist/x402/hub.d.ts.map +1 -0
- package/dist/x402/hub.js +164 -0
- package/dist/x402/hub.js.map +1 -0
- package/dist/x402/index.d.ts +27 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +27 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/proxy.d.ts +162 -0
- package/dist/x402/proxy.d.ts.map +1 -0
- package/dist/x402/proxy.js +198 -0
- package/dist/x402/proxy.js.map +1 -0
- package/dist/x402/server.d.ts +162 -0
- package/dist/x402/server.d.ts.map +1 -0
- package/dist/x402/server.js +306 -0
- package/dist/x402/server.js.map +1 -0
- package/dist/x402/wire.d.ts +104 -0
- package/dist/x402/wire.d.ts.map +1 -0
- package/dist/x402/wire.js +265 -0
- package/dist/x402/wire.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an MCP client keeps its server list, and how `tab connect` edits it.
|
|
3
|
+
*
|
|
4
|
+
* ## The claim this file has to earn
|
|
5
|
+
*
|
|
6
|
+
* Adopting Tab costs one line of configuration. That line is a `mcpServers`
|
|
7
|
+
* stanza in a file the client already owns, and `tab connect` writes it. The
|
|
8
|
+
* claim is only worth making if the write is safe to run, so three properties
|
|
9
|
+
* are enforced here rather than documented:
|
|
10
|
+
*
|
|
11
|
+
* - **Idempotent.** Running it twice writes once. The second run compares the
|
|
12
|
+
* merged document against what is on disk and, finding no change, writes
|
|
13
|
+
* nothing and takes no backup. A connect command that rewrote the file every
|
|
14
|
+
* time would churn a file the client also writes.
|
|
15
|
+
* - **Backed up.** Any run that does change the file copies the previous
|
|
16
|
+
* contents beside it first, named with the instant. This is somebody's editor
|
|
17
|
+
* configuration, and it may hold servers this package has never heard of.
|
|
18
|
+
* - **Never a key.** {@link assertNoSecret} walks the stanza before it is
|
|
19
|
+
* written and refuses on anything key-shaped. The settlement signing key is
|
|
20
|
+
* read from the environment at the moment a Settlement is signed. It does not
|
|
21
|
+
* belong in a JSON file that a desktop client rewrites, syncs, and quotes in
|
|
22
|
+
* its own logs.
|
|
23
|
+
*
|
|
24
|
+
* ## Only `mcpServers.tab` is touched
|
|
25
|
+
*
|
|
26
|
+
* The merge is by key, and every other server in the document is carried through
|
|
27
|
+
* untouched, as is every top-level key the client keeps beside `mcpServers`. A
|
|
28
|
+
* file that fails to parse is not overwritten: it is reported, because a
|
|
29
|
+
* half-written config file is a client that will not start and the caller is the
|
|
30
|
+
* only one who knows what was in it.
|
|
31
|
+
*
|
|
32
|
+
* Requirements: 25.5, 25.6
|
|
33
|
+
*/
|
|
34
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { homedir, platform } from "node:os";
|
|
36
|
+
import { dirname, join, resolve } from "node:path";
|
|
37
|
+
import { ok } from "../_shared/index.js";
|
|
38
|
+
import { fail, notFoundError, validationError } from "../errors.js";
|
|
39
|
+
import { isRecord } from "../mcp/json.js";
|
|
40
|
+
const home = () => homedir();
|
|
41
|
+
/**
|
|
42
|
+
* The per-platform locations, one entry per client.
|
|
43
|
+
*
|
|
44
|
+
* Each path is the client's own documented location. A client absent from a
|
|
45
|
+
* platform has `undefined` there rather than a guessed path, so `--client` names
|
|
46
|
+
* something real or fails by name.
|
|
47
|
+
*/
|
|
48
|
+
function configPathFor(id, os, env, cwd) {
|
|
49
|
+
const appData = env["APPDATA"];
|
|
50
|
+
switch (id) {
|
|
51
|
+
case "claude-desktop":
|
|
52
|
+
if (os === "darwin")
|
|
53
|
+
return join(home(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
54
|
+
if (os === "win32")
|
|
55
|
+
return appData === undefined ? undefined : join(appData, "Claude", "claude_desktop_config.json");
|
|
56
|
+
return join(home(), ".config", "Claude", "claude_desktop_config.json");
|
|
57
|
+
case "claude-code":
|
|
58
|
+
// Project-scoped and checked in beside the code, which is the form a team
|
|
59
|
+
// shares. The user-scoped list lives in `~/.claude.json` and is edited by
|
|
60
|
+
// the CLI itself, so this command does not reach into it.
|
|
61
|
+
return resolve(cwd, ".mcp.json");
|
|
62
|
+
case "cursor":
|
|
63
|
+
return join(home(), ".cursor", "mcp.json");
|
|
64
|
+
case "windsurf":
|
|
65
|
+
return join(home(), ".codeium", "windsurf", "mcp_config.json");
|
|
66
|
+
case "vscode":
|
|
67
|
+
if (os === "darwin")
|
|
68
|
+
return join(home(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
69
|
+
if (os === "win32")
|
|
70
|
+
return appData === undefined ? undefined : join(appData, "Code", "User", "mcp.json");
|
|
71
|
+
return join(home(), ".config", "Code", "User", "mcp.json");
|
|
72
|
+
default:
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Every client `connect` can target, in the order detection prefers them. */
|
|
77
|
+
export const MCP_CLIENT_IDS = ["claude-code", "claude-desktop", "cursor", "windsurf", "vscode"];
|
|
78
|
+
const LABELS = {
|
|
79
|
+
"claude-code": "Claude Code (project .mcp.json)",
|
|
80
|
+
"claude-desktop": "Claude Desktop",
|
|
81
|
+
cursor: "Cursor",
|
|
82
|
+
windsurf: "Windsurf",
|
|
83
|
+
vscode: "Visual Studio Code",
|
|
84
|
+
};
|
|
85
|
+
const exists = (path) => {
|
|
86
|
+
if (path === undefined)
|
|
87
|
+
return false;
|
|
88
|
+
try {
|
|
89
|
+
return existsSync(path) && statSync(path).isFile();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const directoryExists = (path) => {
|
|
96
|
+
if (path === undefined)
|
|
97
|
+
return false;
|
|
98
|
+
try {
|
|
99
|
+
return existsSync(dirname(path));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/** Every known client on this platform, whether or not it is installed. */
|
|
106
|
+
export function detectMcpClients(options = {}) {
|
|
107
|
+
const os = options.os ?? platform();
|
|
108
|
+
const env = options.env ?? process.env;
|
|
109
|
+
const cwd = options.cwd ?? process.cwd();
|
|
110
|
+
return MCP_CLIENT_IDS.map((id) => {
|
|
111
|
+
const configPath = configPathFor(id, os, env, cwd);
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
label: LABELS[id] ?? id,
|
|
115
|
+
configPath,
|
|
116
|
+
present: exists(configPath),
|
|
117
|
+
installed: directoryExists(configPath),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The client to write to when the caller named none.
|
|
123
|
+
*
|
|
124
|
+
* A client with an existing config file wins over one that is merely installed,
|
|
125
|
+
* because an existing file is evidence somebody uses it. Detection never picks
|
|
126
|
+
* between two files silently: with more than one candidate present, the caller
|
|
127
|
+
* is asked to name one.
|
|
128
|
+
*/
|
|
129
|
+
export function chooseMcpClient(targets) {
|
|
130
|
+
const present = targets.filter((target) => target.present);
|
|
131
|
+
if (present.length === 1)
|
|
132
|
+
return ok(present[0]);
|
|
133
|
+
if (present.length > 1) {
|
|
134
|
+
return fail("CONFLICT", "CLIENT_AMBIGUOUS", `more than one MCP client is configured on this machine (${present.map((t) => t.id).join(", ")}); name one with --client`, { details: { candidates: present.map((t) => t.id).join(", ") } });
|
|
135
|
+
}
|
|
136
|
+
const installed = targets.filter((target) => target.installed);
|
|
137
|
+
if (installed.length === 1)
|
|
138
|
+
return ok(installed[0]);
|
|
139
|
+
if (installed.length > 1) {
|
|
140
|
+
return fail("CONFLICT", "CLIENT_AMBIGUOUS", `more than one MCP client is installed on this machine (${installed.map((t) => t.id).join(", ")}); name one with --client`, { details: { candidates: installed.map((t) => t.id).join(", ") } });
|
|
141
|
+
}
|
|
142
|
+
return notFoundError("CLIENT_NOT_FOUND", `no MCP client config was found on this machine; name one with --client (${MCP_CLIENT_IDS.join(", ")}) or point at the file with --config`);
|
|
143
|
+
}
|
|
144
|
+
/** The key the stanza is written under. One server, one name, always this one. */
|
|
145
|
+
export const TAB_SERVER_KEY = "tab";
|
|
146
|
+
/**
|
|
147
|
+
* Builds the stanza.
|
|
148
|
+
*
|
|
149
|
+
* `npx -y @tabai/sdk mcp` rather than a path: the client launches it, `npx`
|
|
150
|
+
* resolves the published package, and nothing has to be installed first. That is
|
|
151
|
+
* the one line the adoption claim rests on.
|
|
152
|
+
*
|
|
153
|
+
* The `env` block carries only what the server reads and a person could publish:
|
|
154
|
+
* an RPC endpoint, a chain id and a read API URL. The names are the ones in the
|
|
155
|
+
* tracked environment contract, `MONAD_RPC_URL`, `MONAD_CHAIN_ID` and
|
|
156
|
+
* `NEXT_PUBLIC_REGISTRY_API_URL`, so a stanza and a `.env` agree.
|
|
157
|
+
*/
|
|
158
|
+
export function buildTabStanza(env = {}) {
|
|
159
|
+
const entries = Object.entries(env).filter(([, value]) => typeof value === "string" && value.trim() !== "");
|
|
160
|
+
return {
|
|
161
|
+
command: "npx",
|
|
162
|
+
args: ["-y", "@tabai/sdk", "mcp"],
|
|
163
|
+
...(entries.length === 0 ? {} : { env: Object.fromEntries(entries) }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Anything that looks like a signing key, by name or by shape. */
|
|
167
|
+
const SECRET_NAME = /(PRIVATE[_-]?KEY|SECRET|MNEMONIC|SEED[_-]?PHRASE|PASSWORD|API[_-]?KEY|TOKEN|CREDENTIAL)/i;
|
|
168
|
+
const SECRET_SHAPE = /^(0x)?[0-9a-fA-F]{64}$/;
|
|
169
|
+
/**
|
|
170
|
+
* Refuses a stanza that carries a secret.
|
|
171
|
+
*
|
|
172
|
+
* Both a name check and a shape check, because either alone is defeated by the
|
|
173
|
+
* obvious mistake: a key under an innocuous name has the shape, and an empty
|
|
174
|
+
* placeholder under `PRIVATE_KEY` has the name. This runs on the merged document
|
|
175
|
+
* before it is written, so it also catches a secret that was already in the file
|
|
176
|
+
* under the `tab` key from an earlier hand edit.
|
|
177
|
+
*/
|
|
178
|
+
export function assertNoSecret(stanza, where = "the tab stanza") {
|
|
179
|
+
const problems = [];
|
|
180
|
+
const walk = (value, path) => {
|
|
181
|
+
if (typeof value === "string") {
|
|
182
|
+
if (SECRET_SHAPE.test(value.trim()))
|
|
183
|
+
problems.push(`${path} holds a 32-byte hex value, which is the shape of a signing key`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
value.forEach((entry, index) => walk(entry, `${path}[${index}]`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (isRecord(value)) {
|
|
191
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
192
|
+
const child = path === "" ? key : `${path}.${key}`;
|
|
193
|
+
if (SECRET_NAME.test(key))
|
|
194
|
+
problems.push(`${child} names a credential`);
|
|
195
|
+
walk(entry, child);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
walk(stanza, "");
|
|
200
|
+
if (problems.length > 0) {
|
|
201
|
+
return validationError("STANZA_CARRIES_SECRET", `${where} would carry a credential and was not written: ${problems.join("; ")}. Settlement signing keys are read from the environment at run time and never from a client config file.`, { details: { problems: problems.join("; ") } });
|
|
202
|
+
}
|
|
203
|
+
return ok(undefined);
|
|
204
|
+
}
|
|
205
|
+
/** Two-space JSON with a trailing newline, which is what every one of these files uses. */
|
|
206
|
+
export const renderConfig = (document) => `${JSON.stringify(document, null, 2)}\n`;
|
|
207
|
+
/**
|
|
208
|
+
* Merges the stanza into a document, leaving every other key alone.
|
|
209
|
+
*
|
|
210
|
+
* `changed` compares rendered text rather than object identity, so a run that
|
|
211
|
+
* produces the same file is reported as no change even when the objects differ
|
|
212
|
+
* by key order.
|
|
213
|
+
*/
|
|
214
|
+
export function mergeTabStanza(previous, stanza) {
|
|
215
|
+
const existingServers = previous["mcpServers"];
|
|
216
|
+
if (existingServers !== undefined && !isRecord(existingServers)) {
|
|
217
|
+
return validationError("CONFIG_MCP_SERVERS_INVALID", "the config file has an `mcpServers` key that is not an object, so it was left alone rather than replaced");
|
|
218
|
+
}
|
|
219
|
+
const servers = { ...(isRecord(existingServers) ? existingServers : {}) };
|
|
220
|
+
const hadStanza = servers[TAB_SERVER_KEY] !== undefined;
|
|
221
|
+
servers[TAB_SERVER_KEY] = stanza;
|
|
222
|
+
// Only the `tab` stanza is checked. Another server's API key is that server's
|
|
223
|
+
// business and carrying it through untouched is the whole point of merging
|
|
224
|
+
// rather than replacing -- refusing on it would make `connect` fail on exactly
|
|
225
|
+
// the machines that already use MCP. The guarantee this command owes is about
|
|
226
|
+
// what it writes, and what it writes is the stanza.
|
|
227
|
+
const secret = assertNoSecret(servers[TAB_SERVER_KEY], `the \`${TAB_SERVER_KEY}\` stanza`);
|
|
228
|
+
if (!secret.ok)
|
|
229
|
+
return secret;
|
|
230
|
+
const merged = { ...previous, mcpServers: servers };
|
|
231
|
+
return ok({
|
|
232
|
+
merged,
|
|
233
|
+
previous,
|
|
234
|
+
changed: renderConfig(previous) !== renderConfig(merged),
|
|
235
|
+
hadStanza,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Reads a client config.
|
|
240
|
+
*
|
|
241
|
+
* A file that does not exist is an empty document and not an error: connecting a
|
|
242
|
+
* client that has never been configured is the common case. A file that exists
|
|
243
|
+
* and does not parse *is* an error, and nothing is written over it.
|
|
244
|
+
*/
|
|
245
|
+
export function readClientConfig(path) {
|
|
246
|
+
if (!existsSync(path))
|
|
247
|
+
return ok({ document: {}, existed: false, raw: "" });
|
|
248
|
+
let raw;
|
|
249
|
+
try {
|
|
250
|
+
raw = readFileSync(path, "utf8");
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return fail("UNAVAILABLE", "CONFIG_UNREADABLE", `\`${path}\` could not be read`, {
|
|
254
|
+
details: { path },
|
|
255
|
+
cause: { code: "READ_FAILED", message: error instanceof Error ? error.message : String(error) },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (raw.trim() === "")
|
|
259
|
+
return ok({ document: {}, existed: true, raw });
|
|
260
|
+
let parsed;
|
|
261
|
+
try {
|
|
262
|
+
parsed = JSON.parse(raw);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
return validationError("CONFIG_MALFORMED", `\`${path}\` is not valid JSON, so it was left untouched: ${error instanceof Error ? error.message : String(error)}`, { details: { path } });
|
|
266
|
+
}
|
|
267
|
+
if (!isRecord(parsed)) {
|
|
268
|
+
return validationError("CONFIG_MALFORMED", `\`${path}\` must hold a JSON object at its top level`, {
|
|
269
|
+
details: { path },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return ok({ document: parsed, existed: true, raw });
|
|
273
|
+
}
|
|
274
|
+
/** Where a backup of `path` goes, named with the instant so two runs never collide. */
|
|
275
|
+
export const backupPathFor = (path, at) => `${path}.tab-backup-${at.toISOString().replace(/[:.]/g, "-")}`;
|
|
276
|
+
/** Writes the merged document, taking a backup first when a file was already there. */
|
|
277
|
+
export function writeClientConfig(path, outcome, existed, at) {
|
|
278
|
+
if (!outcome.changed)
|
|
279
|
+
return ok({ path, wrote: false });
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return fail("UNAVAILABLE", "CONFIG_DIRECTORY_UNWRITABLE", `\`${dirname(path)}\` could not be created`, {
|
|
285
|
+
details: { path },
|
|
286
|
+
cause: { code: "MKDIR_FAILED", message: error instanceof Error ? error.message : String(error) },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
let backupPath;
|
|
290
|
+
if (existed) {
|
|
291
|
+
backupPath = backupPathFor(path, at);
|
|
292
|
+
try {
|
|
293
|
+
copyFileSync(path, backupPath);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return fail("UNAVAILABLE", "CONFIG_BACKUP_FAILED", `\`${path}\` could not be backed up, so it was not changed`, {
|
|
297
|
+
details: { path, backupPath },
|
|
298
|
+
cause: { code: "COPY_FAILED", message: error instanceof Error ? error.message : String(error) },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
writeFileSync(path, renderConfig(outcome.merged), "utf8");
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
return fail("UNAVAILABLE", "CONFIG_UNWRITABLE", `\`${path}\` could not be written`, {
|
|
307
|
+
details: { path },
|
|
308
|
+
cause: { code: "WRITE_FAILED", message: error instanceof Error ? error.message : String(error) },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return ok({ path, wrote: true, ...(backupPath === undefined ? {} : { backupPath }) });
|
|
312
|
+
}
|
|
313
|
+
// ---------------------------------------------------------------- the diff
|
|
314
|
+
/**
|
|
315
|
+
* A unified diff of two JSON documents, line by line.
|
|
316
|
+
*
|
|
317
|
+
* Written here rather than pulled in because it is a longest-common-subsequence
|
|
318
|
+
* over at most a few hundred lines and the alternative is a dependency for a
|
|
319
|
+
* command that prints one. `connect` shows this before it claims to have done
|
|
320
|
+
* anything, because a command that edits a file the user did not open owes them
|
|
321
|
+
* a look at what it changed.
|
|
322
|
+
*/
|
|
323
|
+
export function unifiedDiff(before, after, context = 2) {
|
|
324
|
+
const a = before === "" ? [] : before.replace(/\n$/, "").split("\n");
|
|
325
|
+
const b = after === "" ? [] : after.replace(/\n$/, "").split("\n");
|
|
326
|
+
// LCS table. Both sides are one JSON document, so the quadratic cost is a few
|
|
327
|
+
// hundred thousand cells at worst and is paid once, on a command a person ran.
|
|
328
|
+
const lengths = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
329
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
330
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
331
|
+
lengths[i][j] = a[i] === b[j] ? lengths[i + 1][j + 1] + 1 : Math.max(lengths[i + 1][j], lengths[i][j + 1]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const lines = [];
|
|
335
|
+
let i = 0;
|
|
336
|
+
let j = 0;
|
|
337
|
+
while (i < a.length && j < b.length) {
|
|
338
|
+
if (a[i] === b[j]) {
|
|
339
|
+
lines.push({ mark: " ", text: a[i] });
|
|
340
|
+
i += 1;
|
|
341
|
+
j += 1;
|
|
342
|
+
}
|
|
343
|
+
else if (lengths[i + 1][j] >= lengths[i][j + 1]) {
|
|
344
|
+
lines.push({ mark: "-", text: a[i] });
|
|
345
|
+
i += 1;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
lines.push({ mark: "+", text: b[j] });
|
|
349
|
+
j += 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (; i < a.length; i += 1)
|
|
353
|
+
lines.push({ mark: "-", text: a[i] });
|
|
354
|
+
for (; j < b.length; j += 1)
|
|
355
|
+
lines.push({ mark: "+", text: b[j] });
|
|
356
|
+
// Keep only the changed lines and `context` lines either side, so a large
|
|
357
|
+
// config with one added server prints as one hunk.
|
|
358
|
+
const keep = new Set();
|
|
359
|
+
lines.forEach((line, index) => {
|
|
360
|
+
if (line.mark === " ")
|
|
361
|
+
return;
|
|
362
|
+
for (let k = Math.max(0, index - context); k <= Math.min(lines.length - 1, index + context); k += 1)
|
|
363
|
+
keep.add(k);
|
|
364
|
+
});
|
|
365
|
+
// Nothing changed is no diff at all, not one elision marker.
|
|
366
|
+
if (keep.size === 0)
|
|
367
|
+
return "";
|
|
368
|
+
const rendered = [];
|
|
369
|
+
let skipping = false;
|
|
370
|
+
lines.forEach((line, index) => {
|
|
371
|
+
if (keep.has(index)) {
|
|
372
|
+
rendered.push(`${line.mark}${line.text}`);
|
|
373
|
+
skipping = false;
|
|
374
|
+
}
|
|
375
|
+
else if (!skipping) {
|
|
376
|
+
rendered.push("@@");
|
|
377
|
+
skipping = true;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return rendered.join("\n");
|
|
381
|
+
}
|
|
382
|
+
//# sourceMappingURL=client-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-config.js","sourceRoot":"","sources":["../../src/cli/client-config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where an MCP client keeps its server list, and how `tab connect` edits it.\n *\n * ## The claim this file has to earn\n *\n * Adopting Tab costs one line of configuration. That line is a `mcpServers`\n * stanza in a file the client already owns, and `tab connect` writes it. The\n * claim is only worth making if the write is safe to run, so three properties\n * are enforced here rather than documented:\n *\n * - **Idempotent.** Running it twice writes once. The second run compares the\n * merged document against what is on disk and, finding no change, writes\n * nothing and takes no backup. A connect command that rewrote the file every\n * time would churn a file the client also writes.\n * - **Backed up.** Any run that does change the file copies the previous\n * contents beside it first, named with the instant. This is somebody's editor\n * configuration, and it may hold servers this package has never heard of.\n * - **Never a key.** {@link assertNoSecret} walks the stanza before it is\n * written and refuses on anything key-shaped. The settlement signing key is\n * read from the environment at the moment a Settlement is signed. It does not\n * belong in a JSON file that a desktop client rewrites, syncs, and quotes in\n * its own logs.\n *\n * ## Only `mcpServers.tab` is touched\n *\n * The merge is by key, and every other server in the document is carried through\n * untouched, as is every top-level key the client keeps beside `mcpServers`. A\n * file that fails to parse is not overwritten: it is reported, because a\n * half-written config file is a client that will not start and the caller is the\n * only one who knows what was in it.\n *\n * Requirements: 25.5, 25.6\n */\n\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { homedir, platform } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\n\nimport type { Result } from \"../_shared/index.js\";\nimport { ok } from \"../_shared/index.js\";\n\nimport { fail, notFoundError, validationError } from \"../errors.js\";\nimport { isRecord, type JsonRecord } from \"../mcp/json.js\";\n\n/** One MCP client this command knows how to find. */\nexport interface McpClientTarget {\n /** The `--client` value, for example `claude-desktop`. */\n readonly id: string;\n /** What to call it in a message a person reads. */\n readonly label: string;\n /** Absolute path to its server list on this platform, or undefined where it has none. */\n readonly configPath: string | undefined;\n /** True when the file exists. A known client with no file yet is still a valid target. */\n readonly present: boolean;\n /**\n * True when the directory the file would live in exists, which is the signal\n * that the client is installed even though it has never been configured.\n */\n readonly installed: boolean;\n}\n\nconst home = (): string => homedir();\n\n/**\n * The per-platform locations, one entry per client.\n *\n * Each path is the client's own documented location. A client absent from a\n * platform has `undefined` there rather than a guessed path, so `--client` names\n * something real or fails by name.\n */\nfunction configPathFor(id: string, os: string, env: NodeJS.ProcessEnv, cwd: string): string | undefined {\n const appData = env[\"APPDATA\"];\n switch (id) {\n case \"claude-desktop\":\n if (os === \"darwin\") return join(home(), \"Library\", \"Application Support\", \"Claude\", \"claude_desktop_config.json\");\n if (os === \"win32\") return appData === undefined ? undefined : join(appData, \"Claude\", \"claude_desktop_config.json\");\n return join(home(), \".config\", \"Claude\", \"claude_desktop_config.json\");\n case \"claude-code\":\n // Project-scoped and checked in beside the code, which is the form a team\n // shares. The user-scoped list lives in `~/.claude.json` and is edited by\n // the CLI itself, so this command does not reach into it.\n return resolve(cwd, \".mcp.json\");\n case \"cursor\":\n return join(home(), \".cursor\", \"mcp.json\");\n case \"windsurf\":\n return join(home(), \".codeium\", \"windsurf\", \"mcp_config.json\");\n case \"vscode\":\n if (os === \"darwin\") return join(home(), \"Library\", \"Application Support\", \"Code\", \"User\", \"mcp.json\");\n if (os === \"win32\") return appData === undefined ? undefined : join(appData, \"Code\", \"User\", \"mcp.json\");\n return join(home(), \".config\", \"Code\", \"User\", \"mcp.json\");\n default:\n return undefined;\n }\n}\n\n/** Every client `connect` can target, in the order detection prefers them. */\nexport const MCP_CLIENT_IDS = [\"claude-code\", \"claude-desktop\", \"cursor\", \"windsurf\", \"vscode\"] as const;\n\nconst LABELS: Readonly<Record<string, string>> = {\n \"claude-code\": \"Claude Code (project .mcp.json)\",\n \"claude-desktop\": \"Claude Desktop\",\n cursor: \"Cursor\",\n windsurf: \"Windsurf\",\n vscode: \"Visual Studio Code\",\n};\n\nexport interface DetectOptions {\n readonly os?: string;\n readonly env?: NodeJS.ProcessEnv;\n readonly cwd?: string;\n}\n\nconst exists = (path: string | undefined): boolean => {\n if (path === undefined) return false;\n try {\n return existsSync(path) && statSync(path).isFile();\n } catch {\n return false;\n }\n};\n\nconst directoryExists = (path: string | undefined): boolean => {\n if (path === undefined) return false;\n try {\n return existsSync(dirname(path));\n } catch {\n return false;\n }\n};\n\n/** Every known client on this platform, whether or not it is installed. */\nexport function detectMcpClients(options: DetectOptions = {}): readonly McpClientTarget[] {\n const os = options.os ?? platform();\n const env = options.env ?? process.env;\n const cwd = options.cwd ?? process.cwd();\n return MCP_CLIENT_IDS.map((id) => {\n const configPath = configPathFor(id, os, env, cwd);\n return {\n id,\n label: LABELS[id] ?? id,\n configPath,\n present: exists(configPath),\n installed: directoryExists(configPath),\n };\n });\n}\n\n/**\n * The client to write to when the caller named none.\n *\n * A client with an existing config file wins over one that is merely installed,\n * because an existing file is evidence somebody uses it. Detection never picks\n * between two files silently: with more than one candidate present, the caller\n * is asked to name one.\n */\nexport function chooseMcpClient(\n targets: readonly McpClientTarget[],\n): Result<McpClientTarget> {\n const present = targets.filter((target) => target.present);\n if (present.length === 1) return ok(present[0]!);\n if (present.length > 1) {\n return fail(\n \"CONFLICT\",\n \"CLIENT_AMBIGUOUS\",\n `more than one MCP client is configured on this machine (${present.map((t) => t.id).join(\", \")}); name one with --client`,\n { details: { candidates: present.map((t) => t.id).join(\", \") } },\n );\n }\n const installed = targets.filter((target) => target.installed);\n if (installed.length === 1) return ok(installed[0]!);\n if (installed.length > 1) {\n return fail(\n \"CONFLICT\",\n \"CLIENT_AMBIGUOUS\",\n `more than one MCP client is installed on this machine (${installed.map((t) => t.id).join(\", \")}); name one with --client`,\n { details: { candidates: installed.map((t) => t.id).join(\", \") } },\n );\n }\n return notFoundError(\n \"CLIENT_NOT_FOUND\",\n `no MCP client config was found on this machine; name one with --client (${MCP_CLIENT_IDS.join(\", \")}) or point at the file with --config`,\n );\n}\n\n// ---------------------------------------------------------------- the stanza\n\n/** The `env` block the stanza carries. Non-secret values only, by construction. */\nexport type StanzaEnv = Readonly<Record<string, string>>;\n\nexport interface TabStanza {\n readonly command: string;\n readonly args: readonly string[];\n readonly env?: StanzaEnv;\n}\n\n/** The key the stanza is written under. One server, one name, always this one. */\nexport const TAB_SERVER_KEY = \"tab\";\n\n/**\n * Builds the stanza.\n *\n * `npx -y @tabai/sdk mcp` rather than a path: the client launches it, `npx`\n * resolves the published package, and nothing has to be installed first. That is\n * the one line the adoption claim rests on.\n *\n * The `env` block carries only what the server reads and a person could publish:\n * an RPC endpoint, a chain id and a read API URL. The names are the ones in the\n * tracked environment contract, `MONAD_RPC_URL`, `MONAD_CHAIN_ID` and\n * `NEXT_PUBLIC_REGISTRY_API_URL`, so a stanza and a `.env` agree.\n */\nexport function buildTabStanza(env: StanzaEnv = {}): TabStanza {\n const entries = Object.entries(env).filter(([, value]) => typeof value === \"string\" && value.trim() !== \"\");\n return {\n command: \"npx\",\n args: [\"-y\", \"@tabai/sdk\", \"mcp\"],\n ...(entries.length === 0 ? {} : { env: Object.fromEntries(entries) }),\n };\n}\n\n/** Anything that looks like a signing key, by name or by shape. */\nconst SECRET_NAME = /(PRIVATE[_-]?KEY|SECRET|MNEMONIC|SEED[_-]?PHRASE|PASSWORD|API[_-]?KEY|TOKEN|CREDENTIAL)/i;\nconst SECRET_SHAPE = /^(0x)?[0-9a-fA-F]{64}$/;\n\n/**\n * Refuses a stanza that carries a secret.\n *\n * Both a name check and a shape check, because either alone is defeated by the\n * obvious mistake: a key under an innocuous name has the shape, and an empty\n * placeholder under `PRIVATE_KEY` has the name. This runs on the merged document\n * before it is written, so it also catches a secret that was already in the file\n * under the `tab` key from an earlier hand edit.\n */\nexport function assertNoSecret(stanza: unknown, where = \"the tab stanza\"): Result<void> {\n const problems: string[] = [];\n const walk = (value: unknown, path: string): void => {\n if (typeof value === \"string\") {\n if (SECRET_SHAPE.test(value.trim())) problems.push(`${path} holds a 32-byte hex value, which is the shape of a signing key`);\n return;\n }\n if (Array.isArray(value)) {\n value.forEach((entry, index) => walk(entry, `${path}[${index}]`));\n return;\n }\n if (isRecord(value)) {\n for (const [key, entry] of Object.entries(value)) {\n const child = path === \"\" ? key : `${path}.${key}`;\n if (SECRET_NAME.test(key)) problems.push(`${child} names a credential`);\n walk(entry, child);\n }\n }\n };\n walk(stanza, \"\");\n if (problems.length > 0) {\n return validationError(\n \"STANZA_CARRIES_SECRET\",\n `${where} would carry a credential and was not written: ${problems.join(\"; \")}. Settlement signing keys are read from the environment at run time and never from a client config file.`,\n { details: { problems: problems.join(\"; \") } },\n );\n }\n return ok(undefined);\n}\n\n// ---------------------------------------------------------------- merge\n\nexport interface MergeOutcome {\n /** The document as it should be on disk. */\n readonly merged: JsonRecord;\n /** The document that was on disk, or an empty object when the file did not exist. */\n readonly previous: JsonRecord;\n /** False when the file already said exactly this, which is the idempotent case. */\n readonly changed: boolean;\n /** Whether a `tab` stanza was already there, so a message can say added or updated. */\n readonly hadStanza: boolean;\n}\n\n/** Two-space JSON with a trailing newline, which is what every one of these files uses. */\nexport const renderConfig = (document: unknown): string => `${JSON.stringify(document, null, 2)}\\n`;\n\n/**\n * Merges the stanza into a document, leaving every other key alone.\n *\n * `changed` compares rendered text rather than object identity, so a run that\n * produces the same file is reported as no change even when the objects differ\n * by key order.\n */\nexport function mergeTabStanza(previous: JsonRecord, stanza: TabStanza): Result<MergeOutcome> {\n const existingServers = previous[\"mcpServers\"];\n if (existingServers !== undefined && !isRecord(existingServers)) {\n return validationError(\n \"CONFIG_MCP_SERVERS_INVALID\",\n \"the config file has an `mcpServers` key that is not an object, so it was left alone rather than replaced\",\n );\n }\n const servers: Record<string, unknown> = { ...(isRecord(existingServers) ? existingServers : {}) };\n const hadStanza = servers[TAB_SERVER_KEY] !== undefined;\n servers[TAB_SERVER_KEY] = stanza;\n\n // Only the `tab` stanza is checked. Another server's API key is that server's\n // business and carrying it through untouched is the whole point of merging\n // rather than replacing -- refusing on it would make `connect` fail on exactly\n // the machines that already use MCP. The guarantee this command owes is about\n // what it writes, and what it writes is the stanza.\n const secret = assertNoSecret(servers[TAB_SERVER_KEY], `the \\`${TAB_SERVER_KEY}\\` stanza`);\n if (!secret.ok) return secret;\n\n const merged: JsonRecord = { ...previous, mcpServers: servers };\n\n return ok({\n merged,\n previous,\n changed: renderConfig(previous) !== renderConfig(merged),\n hadStanza,\n });\n}\n\n// ---------------------------------------------------------------- the file\n\nexport interface ReadConfigOutcome {\n readonly document: JsonRecord;\n readonly existed: boolean;\n readonly raw: string;\n}\n\n/**\n * Reads a client config.\n *\n * A file that does not exist is an empty document and not an error: connecting a\n * client that has never been configured is the common case. A file that exists\n * and does not parse *is* an error, and nothing is written over it.\n */\nexport function readClientConfig(path: string): Result<ReadConfigOutcome> {\n if (!existsSync(path)) return ok({ document: {}, existed: false, raw: \"\" });\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (error) {\n return fail(\"UNAVAILABLE\", \"CONFIG_UNREADABLE\", `\\`${path}\\` could not be read`, {\n details: { path },\n cause: { code: \"READ_FAILED\", message: error instanceof Error ? error.message : String(error) },\n });\n }\n if (raw.trim() === \"\") return ok({ document: {}, existed: true, raw });\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n return validationError(\n \"CONFIG_MALFORMED\",\n `\\`${path}\\` is not valid JSON, so it was left untouched: ${error instanceof Error ? error.message : String(error)}`,\n { details: { path } },\n );\n }\n if (!isRecord(parsed)) {\n return validationError(\"CONFIG_MALFORMED\", `\\`${path}\\` must hold a JSON object at its top level`, {\n details: { path },\n });\n }\n return ok({ document: parsed, existed: true, raw });\n}\n\n/** Where a backup of `path` goes, named with the instant so two runs never collide. */\nexport const backupPathFor = (path: string, at: Date): string =>\n `${path}.tab-backup-${at.toISOString().replace(/[:.]/g, \"-\")}`;\n\nexport interface WriteOutcome {\n readonly path: string;\n /** Absent when nothing was written, which is the idempotent case. */\n readonly backupPath?: string;\n readonly wrote: boolean;\n}\n\n/** Writes the merged document, taking a backup first when a file was already there. */\nexport function writeClientConfig(\n path: string,\n outcome: MergeOutcome,\n existed: boolean,\n at: Date,\n): Result<WriteOutcome> {\n if (!outcome.changed) return ok({ path, wrote: false });\n try {\n mkdirSync(dirname(path), { recursive: true });\n } catch (error) {\n return fail(\"UNAVAILABLE\", \"CONFIG_DIRECTORY_UNWRITABLE\", `\\`${dirname(path)}\\` could not be created`, {\n details: { path },\n cause: { code: \"MKDIR_FAILED\", message: error instanceof Error ? error.message : String(error) },\n });\n }\n\n let backupPath: string | undefined;\n if (existed) {\n backupPath = backupPathFor(path, at);\n try {\n copyFileSync(path, backupPath);\n } catch (error) {\n return fail(\"UNAVAILABLE\", \"CONFIG_BACKUP_FAILED\", `\\`${path}\\` could not be backed up, so it was not changed`, {\n details: { path, backupPath },\n cause: { code: \"COPY_FAILED\", message: error instanceof Error ? error.message : String(error) },\n });\n }\n }\n\n try {\n writeFileSync(path, renderConfig(outcome.merged), \"utf8\");\n } catch (error) {\n return fail(\"UNAVAILABLE\", \"CONFIG_UNWRITABLE\", `\\`${path}\\` could not be written`, {\n details: { path },\n cause: { code: \"WRITE_FAILED\", message: error instanceof Error ? error.message : String(error) },\n });\n }\n return ok({ path, wrote: true, ...(backupPath === undefined ? {} : { backupPath }) });\n}\n\n// ---------------------------------------------------------------- the diff\n\n/**\n * A unified diff of two JSON documents, line by line.\n *\n * Written here rather than pulled in because it is a longest-common-subsequence\n * over at most a few hundred lines and the alternative is a dependency for a\n * command that prints one. `connect` shows this before it claims to have done\n * anything, because a command that edits a file the user did not open owes them\n * a look at what it changed.\n */\nexport function unifiedDiff(before: string, after: string, context = 2): string {\n const a = before === \"\" ? [] : before.replace(/\\n$/, \"\").split(\"\\n\");\n const b = after === \"\" ? [] : after.replace(/\\n$/, \"\").split(\"\\n\");\n\n // LCS table. Both sides are one JSON document, so the quadratic cost is a few\n // hundred thousand cells at worst and is paid once, on a command a person ran.\n const lengths: number[][] = Array.from({ length: a.length + 1 }, () => new Array<number>(b.length + 1).fill(0));\n for (let i = a.length - 1; i >= 0; i -= 1) {\n for (let j = b.length - 1; j >= 0; j -= 1) {\n lengths[i]![j] = a[i] === b[j] ? lengths[i + 1]![j + 1]! + 1 : Math.max(lengths[i + 1]![j]!, lengths[i]![j + 1]!);\n }\n }\n\n const lines: { readonly mark: \" \" | \"-\" | \"+\"; readonly text: string }[] = [];\n let i = 0;\n let j = 0;\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n lines.push({ mark: \" \", text: a[i]! });\n i += 1;\n j += 1;\n } else if (lengths[i + 1]![j]! >= lengths[i]![j + 1]!) {\n lines.push({ mark: \"-\", text: a[i]! });\n i += 1;\n } else {\n lines.push({ mark: \"+\", text: b[j]! });\n j += 1;\n }\n }\n for (; i < a.length; i += 1) lines.push({ mark: \"-\", text: a[i]! });\n for (; j < b.length; j += 1) lines.push({ mark: \"+\", text: b[j]! });\n\n // Keep only the changed lines and `context` lines either side, so a large\n // config with one added server prints as one hunk.\n const keep = new Set<number>();\n lines.forEach((line, index) => {\n if (line.mark === \" \") return;\n for (let k = Math.max(0, index - context); k <= Math.min(lines.length - 1, index + context); k += 1) keep.add(k);\n });\n\n // Nothing changed is no diff at all, not one elision marker.\n if (keep.size === 0) return \"\";\n\n const rendered: string[] = [];\n let skipping = false;\n lines.forEach((line, index) => {\n if (keep.has(index)) {\n rendered.push(`${line.mark}${line.text}`);\n skipping = false;\n } else if (!skipping) {\n rendered.push(\"@@\");\n skipping = true;\n }\n });\n return rendered.join(\"\\n\");\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tab connect`: the one line of configuration.
|
|
3
|
+
*
|
|
4
|
+
* The whole adoption story is this command. It finds the MCP client's server
|
|
5
|
+
* list, reads the deployment keylessly so it can tell the caller what they are
|
|
6
|
+
* about to be connected to, merges one `mcpServers.tab` stanza, backs up what
|
|
7
|
+
* was there, and prints the diff.
|
|
8
|
+
*
|
|
9
|
+
* ## Read before write
|
|
10
|
+
*
|
|
11
|
+
* The keyless read happens first and its result is reported whether it succeeded
|
|
12
|
+
* or not. A `connect` that silently wrote a stanza pointing at a registry that
|
|
13
|
+
* does not answer would look like success and behave like a broken install, and
|
|
14
|
+
* the person running it would find out from a model instead of from the command.
|
|
15
|
+
* The read never blocks the write: a deployment that is temporarily down is not
|
|
16
|
+
* a reason to refuse to configure a client against it.
|
|
17
|
+
*
|
|
18
|
+
* ## Nothing here is a secret
|
|
19
|
+
*
|
|
20
|
+
* The stanza carries a command, its arguments, and at most two URLs. Every value
|
|
21
|
+
* in it is publishable. {@link assertNoSecret} runs over the merged document
|
|
22
|
+
* before anything is written, so the property is enforced rather than asserted
|
|
23
|
+
* in a comment.
|
|
24
|
+
*
|
|
25
|
+
* Requirements: 25.5, 25.6
|
|
26
|
+
*/
|
|
27
|
+
import type { Result } from "../_shared/index.js";
|
|
28
|
+
import type { Logger } from "../logger.js";
|
|
29
|
+
import { type RegistryFetch } from "../mcp/registry-client.js";
|
|
30
|
+
import type { TabMcpSettings } from "../mcp/settings.js";
|
|
31
|
+
import { type TabStanza } from "./client-config.js";
|
|
32
|
+
export interface ConnectOptions {
|
|
33
|
+
readonly settings: TabMcpSettings;
|
|
34
|
+
/** `claude-code`, `claude-desktop`, `cursor`, `windsurf` or `vscode`. */
|
|
35
|
+
readonly client?: string;
|
|
36
|
+
/** An explicit config file, which beats `client` and skips detection. */
|
|
37
|
+
readonly configPath?: string;
|
|
38
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
39
|
+
readonly os?: string;
|
|
40
|
+
readonly cwd?: string;
|
|
41
|
+
/** Injectable clock, so a backup name is testable. */
|
|
42
|
+
readonly now?: () => Date;
|
|
43
|
+
/** Work out and print everything, and write nothing. */
|
|
44
|
+
readonly dryRun?: boolean;
|
|
45
|
+
readonly fetchImpl?: RegistryFetch;
|
|
46
|
+
readonly logger?: Logger;
|
|
47
|
+
}
|
|
48
|
+
/** What the deployment said when it was asked, before anything was written. */
|
|
49
|
+
export interface ConnectPreflight {
|
|
50
|
+
readonly registryUrl: string | undefined;
|
|
51
|
+
/** Services indexed, or null when the read API did not answer. */
|
|
52
|
+
readonly services: number | null;
|
|
53
|
+
readonly note: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ConnectReport {
|
|
56
|
+
readonly client: string;
|
|
57
|
+
readonly configPath: string;
|
|
58
|
+
/** False when the file already said exactly this. */
|
|
59
|
+
readonly changed: boolean;
|
|
60
|
+
/** True when a `tab` stanza was already there, so a message can say updated. */
|
|
61
|
+
readonly hadStanza: boolean;
|
|
62
|
+
readonly backupPath: string | undefined;
|
|
63
|
+
readonly stanza: TabStanza;
|
|
64
|
+
/** Empty when nothing changed. */
|
|
65
|
+
readonly diff: string;
|
|
66
|
+
readonly preflight: ConnectPreflight;
|
|
67
|
+
readonly dryRun: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs the command.
|
|
71
|
+
*
|
|
72
|
+
* Every failure is a `Result`, including a config file that will not parse. The
|
|
73
|
+
* one thing this never does is write over something it could not read.
|
|
74
|
+
*/
|
|
75
|
+
export declare function runConnect(options: ConnectOptions): Promise<Result<ConnectReport>>;
|
|
76
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/cli/connect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAWL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAmFD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAkDxF"}
|