@gmickel/gno 1.12.3 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -30
- package/assets/skill/SKILL.md +6 -1
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/context-resolver.ts +285 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/index.ts +3 -3
- package/src/pipeline/answer-prompt.ts +80 -0
- package/src/pipeline/answer.ts +12 -26
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/result-context.ts +51 -0
- package/src/pipeline/search.ts +5 -1
- package/src/pipeline/vsearch.ts +2 -0
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +320 -53
- package/src/store/types.ts +124 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/** Comment-preserving MCP client config readers and targeted editors. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyEdits,
|
|
5
|
+
type FormattingOptions,
|
|
6
|
+
getNodeValue,
|
|
7
|
+
modify,
|
|
8
|
+
type Node,
|
|
9
|
+
type ParseError,
|
|
10
|
+
parseTree,
|
|
11
|
+
printParseErrorCode,
|
|
12
|
+
} from "jsonc-parser";
|
|
13
|
+
|
|
14
|
+
import type { AnyMcpConfig, StandardMcpEntry } from "./config.js";
|
|
15
|
+
|
|
16
|
+
import { CliError } from "../../errors.js";
|
|
17
|
+
|
|
18
|
+
export interface ParsedServerEntry {
|
|
19
|
+
exists: boolean;
|
|
20
|
+
entry?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isPlainRecord(
|
|
24
|
+
value: unknown
|
|
25
|
+
): value is Record<string, unknown> {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const prototype = Object.getPrototypeOf(value);
|
|
30
|
+
return prototype === Object.prototype || prototype === null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateRootAndServers(
|
|
34
|
+
root: unknown,
|
|
35
|
+
serversKey: string,
|
|
36
|
+
configPath: string,
|
|
37
|
+
format: string
|
|
38
|
+
): Record<string, unknown> | undefined {
|
|
39
|
+
if (!isPlainRecord(root)) {
|
|
40
|
+
throw new CliError(
|
|
41
|
+
"RUNTIME",
|
|
42
|
+
`${format} root in ${configPath} must be an object.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const servers = root[serversKey];
|
|
46
|
+
if (servers === undefined) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (!isPlainRecord(servers)) {
|
|
50
|
+
throw new CliError(
|
|
51
|
+
"RUNTIME",
|
|
52
|
+
`${serversKey} in ${configPath} must be an object.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return servers;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function jsoncFormatting(content: string): FormattingOptions {
|
|
59
|
+
const indentMatch = content.match(/\n([ \t]+)\S/u)?.[1];
|
|
60
|
+
return {
|
|
61
|
+
insertSpaces: !indentMatch?.includes("\t"),
|
|
62
|
+
tabSize: indentMatch?.includes("\t") ? 1 : (indentMatch?.length ?? 2),
|
|
63
|
+
eol: content.includes("\r\n") ? "\r\n" : "\n",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ParsedJsoncDocument {
|
|
68
|
+
root: AnyMcpConfig;
|
|
69
|
+
rootNode?: Node;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function jsoncPropertyNodes(objectNode: Node, propertyName: string): Node[] {
|
|
73
|
+
return (objectNode.children ?? []).filter(
|
|
74
|
+
(property) =>
|
|
75
|
+
property.type === "property" &&
|
|
76
|
+
property.children?.[0]?.value === propertyName
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getUniqueJsoncPropertyValue(
|
|
81
|
+
objectNode: Node,
|
|
82
|
+
propertyName: string,
|
|
83
|
+
configPath: string,
|
|
84
|
+
location: string
|
|
85
|
+
): Node | undefined {
|
|
86
|
+
const properties = jsoncPropertyNodes(objectNode, propertyName);
|
|
87
|
+
if (properties.length > 1) {
|
|
88
|
+
throw new CliError(
|
|
89
|
+
"RUNTIME",
|
|
90
|
+
`Ambiguous MCP JSONC config ${configPath}: duplicate "${propertyName}" key in ${location}.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return properties[0]?.children?.[1];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseJsoncRoot(
|
|
97
|
+
content: string,
|
|
98
|
+
configPath: string
|
|
99
|
+
): ParsedJsoncDocument {
|
|
100
|
+
if (!content.trim()) {
|
|
101
|
+
return { root: {} };
|
|
102
|
+
}
|
|
103
|
+
const errors: ParseError[] = [];
|
|
104
|
+
const rootNode = parseTree(content, errors, {
|
|
105
|
+
allowTrailingComma: true,
|
|
106
|
+
disallowComments: false,
|
|
107
|
+
allowEmptyContent: true,
|
|
108
|
+
});
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
const first = errors[0];
|
|
111
|
+
throw new CliError(
|
|
112
|
+
"RUNTIME",
|
|
113
|
+
`Malformed JSONC in ${configPath}: ${first ? printParseErrorCode(first.error) : "parse error"}.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const root = rootNode ? getNodeValue(rootNode) : undefined;
|
|
117
|
+
if (!rootNode || rootNode.type !== "object" || !isPlainRecord(root)) {
|
|
118
|
+
throw new CliError(
|
|
119
|
+
"RUNTIME",
|
|
120
|
+
`JSONC root in ${configPath} must be an object.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return { root: root as AnyMcpConfig, rootNode };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getJsoncServerEntry(
|
|
127
|
+
content: string,
|
|
128
|
+
configPath: string,
|
|
129
|
+
serversKey: string
|
|
130
|
+
): ParsedServerEntry {
|
|
131
|
+
const { root, rootNode } = parseJsoncRoot(content, configPath);
|
|
132
|
+
const serversNode = rootNode
|
|
133
|
+
? getUniqueJsoncPropertyValue(
|
|
134
|
+
rootNode,
|
|
135
|
+
serversKey,
|
|
136
|
+
configPath,
|
|
137
|
+
"the root object"
|
|
138
|
+
)
|
|
139
|
+
: undefined;
|
|
140
|
+
const servers = validateRootAndServers(root, serversKey, configPath, "JSONC");
|
|
141
|
+
if (serversNode?.type === "object") {
|
|
142
|
+
getUniqueJsoncPropertyValue(
|
|
143
|
+
serversNode,
|
|
144
|
+
"gno",
|
|
145
|
+
configPath,
|
|
146
|
+
`the "${serversKey}" server map`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return servers && Object.hasOwn(servers, "gno")
|
|
150
|
+
? { exists: true, entry: servers.gno }
|
|
151
|
+
: { exists: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function setJsoncServerEntry(
|
|
155
|
+
content: string,
|
|
156
|
+
configPath: string,
|
|
157
|
+
serversKey: string,
|
|
158
|
+
entry: unknown
|
|
159
|
+
): string {
|
|
160
|
+
getJsoncServerEntry(content, configPath, serversKey);
|
|
161
|
+
const edits = modify(content, [serversKey, "gno"], entry, {
|
|
162
|
+
formattingOptions: jsoncFormatting(content),
|
|
163
|
+
});
|
|
164
|
+
const updated = applyEdits(content, edits);
|
|
165
|
+
getJsoncServerEntry(updated, configPath, serversKey);
|
|
166
|
+
return updated.endsWith("\n")
|
|
167
|
+
? updated
|
|
168
|
+
: `${updated}${jsoncFormatting(content).eol}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function removeJsoncServerEntry(
|
|
172
|
+
content: string,
|
|
173
|
+
configPath: string,
|
|
174
|
+
serversKey: string
|
|
175
|
+
): { content: string; removed: boolean } {
|
|
176
|
+
const parsed = getJsoncServerEntry(content, configPath, serversKey);
|
|
177
|
+
if (!parsed.exists) {
|
|
178
|
+
return { content, removed: false };
|
|
179
|
+
}
|
|
180
|
+
const { root } = parseJsoncRoot(content, configPath);
|
|
181
|
+
const servers = validateRootAndServers(root, serversKey, configPath, "JSONC");
|
|
182
|
+
const path =
|
|
183
|
+
servers && Object.keys(servers).length === 1
|
|
184
|
+
? [serversKey]
|
|
185
|
+
: [serversKey, "gno"];
|
|
186
|
+
const updated = applyEdits(
|
|
187
|
+
content,
|
|
188
|
+
modify(content, path, undefined, {
|
|
189
|
+
formattingOptions: jsoncFormatting(content),
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
getJsoncServerEntry(updated, configPath, serversKey);
|
|
193
|
+
return { content: updated, removed: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
getYamlServerEntry,
|
|
198
|
+
removeYamlServerEntry,
|
|
199
|
+
setYamlServerEntry,
|
|
200
|
+
} from "./yaml-config-editor.js";
|
|
201
|
+
|
|
202
|
+
const TOML_SECTION_PATTERN = /^\s*\[([^\]]+)]\s*(?:#.*)?$/;
|
|
203
|
+
const TOML_GNO_SECTION_PATTERN =
|
|
204
|
+
/^\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*(?:gno|"gno"|'gno')(?:\s*\.\s*(?:env|"env"|'env'))?\s*$/;
|
|
205
|
+
const TOML_GNO_DESCENDANT_PATTERN =
|
|
206
|
+
/^\s*(?:mcp_servers|"mcp_servers"|'mcp_servers')\s*\.\s*(?:gno|"gno"|'gno')\s*\./;
|
|
207
|
+
|
|
208
|
+
interface TomlLine {
|
|
209
|
+
text: string;
|
|
210
|
+
isTable: boolean;
|
|
211
|
+
isGnoTable: boolean;
|
|
212
|
+
isUnsupportedGnoTable: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isEscaped(value: string, position: number): boolean {
|
|
216
|
+
let slashes = 0;
|
|
217
|
+
for (
|
|
218
|
+
let cursor = position - 1;
|
|
219
|
+
cursor >= 0 && value[cursor] === "\\";
|
|
220
|
+
cursor -= 1
|
|
221
|
+
) {
|
|
222
|
+
slashes += 1;
|
|
223
|
+
}
|
|
224
|
+
return slashes % 2 === 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function nextMultilineState(
|
|
228
|
+
line: string,
|
|
229
|
+
current: '"""' | "'''" | null
|
|
230
|
+
): '"""' | "'''" | null {
|
|
231
|
+
let position = 0;
|
|
232
|
+
if (current) {
|
|
233
|
+
let closing = line.indexOf(current);
|
|
234
|
+
while (closing !== -1 && current === '"""' && isEscaped(line, closing)) {
|
|
235
|
+
closing = line.indexOf(current, closing + 3);
|
|
236
|
+
}
|
|
237
|
+
if (closing === -1) {
|
|
238
|
+
return current;
|
|
239
|
+
}
|
|
240
|
+
position = closing + 3;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
while (position < line.length) {
|
|
244
|
+
const character = line[position];
|
|
245
|
+
if (character === "#") {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (character === '"') {
|
|
249
|
+
if (line.startsWith('"""', position)) {
|
|
250
|
+
let closing = line.indexOf('"""', position + 3);
|
|
251
|
+
while (closing !== -1 && isEscaped(line, closing)) {
|
|
252
|
+
closing = line.indexOf('"""', closing + 3);
|
|
253
|
+
}
|
|
254
|
+
if (closing === -1) {
|
|
255
|
+
return '"""';
|
|
256
|
+
}
|
|
257
|
+
position = closing + 3;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
position += 1;
|
|
261
|
+
while (position < line.length) {
|
|
262
|
+
if (line[position] === '"' && !isEscaped(line, position)) {
|
|
263
|
+
position += 1;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
position += 1;
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (character === "'") {
|
|
271
|
+
if (line.startsWith("'''", position)) {
|
|
272
|
+
const closing = line.indexOf("'''", position + 3);
|
|
273
|
+
if (closing === -1) {
|
|
274
|
+
return "'''";
|
|
275
|
+
}
|
|
276
|
+
position = closing + 3;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const closing = line.indexOf("'", position + 1);
|
|
280
|
+
position = closing === -1 ? line.length : closing + 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
position += 1;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function scanTomlLines(content: string): TomlLine[] {
|
|
289
|
+
let multiline: '"""' | "'''" | null = null;
|
|
290
|
+
return content.split(/\r?\n/).map((text) => {
|
|
291
|
+
const outside = multiline === null;
|
|
292
|
+
const section = outside ? text.match(TOML_SECTION_PATTERN)?.[1] : undefined;
|
|
293
|
+
const line: TomlLine = {
|
|
294
|
+
text,
|
|
295
|
+
isTable: section !== undefined,
|
|
296
|
+
isGnoTable:
|
|
297
|
+
section !== undefined && TOML_GNO_SECTION_PATTERN.test(section),
|
|
298
|
+
isUnsupportedGnoTable:
|
|
299
|
+
section !== undefined &&
|
|
300
|
+
TOML_GNO_DESCENDANT_PATTERN.test(section) &&
|
|
301
|
+
!TOML_GNO_SECTION_PATTERN.test(section),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
multiline = nextMultilineState(text, multiline);
|
|
305
|
+
return line;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function parseTomlRoot(content: string, configPath: string): AnyMcpConfig {
|
|
310
|
+
if (!content.trim()) {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const root = Bun.TOML.parse(content);
|
|
315
|
+
if (!isPlainRecord(root)) {
|
|
316
|
+
throw new Error("non-record root");
|
|
317
|
+
}
|
|
318
|
+
return root as AnyMcpConfig;
|
|
319
|
+
} catch {
|
|
320
|
+
throw new CliError("RUNTIME", `Malformed TOML in ${configPath}.`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function getTomlServerEntry(
|
|
325
|
+
content: string,
|
|
326
|
+
configPath: string
|
|
327
|
+
): ParsedServerEntry {
|
|
328
|
+
const root = parseTomlRoot(content, configPath);
|
|
329
|
+
const servers = validateRootAndServers(
|
|
330
|
+
root,
|
|
331
|
+
"mcp_servers",
|
|
332
|
+
configPath,
|
|
333
|
+
"TOML"
|
|
334
|
+
);
|
|
335
|
+
return servers && Object.hasOwn(servers, "gno")
|
|
336
|
+
? { exists: true, entry: servers.gno }
|
|
337
|
+
: { exists: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function removeTomlGnoSections(
|
|
341
|
+
content: string,
|
|
342
|
+
configPath: string
|
|
343
|
+
): { content: string; removed: boolean; newline: string } {
|
|
344
|
+
const lines = scanTomlLines(content);
|
|
345
|
+
if (lines.some(({ isUnsupportedGnoTable }) => isUnsupportedGnoTable)) {
|
|
346
|
+
throw new CliError(
|
|
347
|
+
"RUNTIME",
|
|
348
|
+
`Unsupported nested GNO MCP entry in ${configPath}.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
352
|
+
const kept: string[] = [];
|
|
353
|
+
let removing = false;
|
|
354
|
+
let removed = false;
|
|
355
|
+
for (const line of lines) {
|
|
356
|
+
if (line.isTable) {
|
|
357
|
+
removing = line.isGnoTable;
|
|
358
|
+
removed ||= removing;
|
|
359
|
+
}
|
|
360
|
+
if (!removing || line.text.trimStart().startsWith("#")) {
|
|
361
|
+
kept.push(line.text);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
while (kept.at(-1) === "") {
|
|
365
|
+
kept.pop();
|
|
366
|
+
}
|
|
367
|
+
return { content: kept.join(newline), removed, newline };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function tomlString(value: string): string {
|
|
371
|
+
return JSON.stringify(value);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function serializeTomlEntry(entry: StandardMcpEntry, newline: string): string {
|
|
375
|
+
const args = entry.args.map(tomlString).join(", ");
|
|
376
|
+
const lines = [
|
|
377
|
+
"[mcp_servers.gno]",
|
|
378
|
+
`command = ${tomlString(entry.command)}`,
|
|
379
|
+
`args = [${args}]`,
|
|
380
|
+
];
|
|
381
|
+
if (entry.env && Object.keys(entry.env).length > 0) {
|
|
382
|
+
lines.push("", "[mcp_servers.gno.env]");
|
|
383
|
+
for (const key of ["GNO_DATA_DIR", "GNO_CACHE_DIR"] as const) {
|
|
384
|
+
const value = entry.env[key];
|
|
385
|
+
if (value) {
|
|
386
|
+
lines.push(`${key} = ${tomlString(value)}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return `${lines.join(newline)}${newline}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function setTomlServerEntry(
|
|
394
|
+
content: string,
|
|
395
|
+
configPath: string,
|
|
396
|
+
entry: StandardMcpEntry
|
|
397
|
+
): string {
|
|
398
|
+
const parsed = getTomlServerEntry(content, configPath);
|
|
399
|
+
const removed = removeTomlGnoSections(content, configPath);
|
|
400
|
+
if (parsed.exists && !removed.removed) {
|
|
401
|
+
throw new CliError(
|
|
402
|
+
"RUNTIME",
|
|
403
|
+
`Unsupported inline GNO MCP entry in ${configPath}.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const prefix = removed.content.trimEnd();
|
|
407
|
+
const updated = prefix
|
|
408
|
+
? `${prefix}${removed.newline}${removed.newline}${serializeTomlEntry(entry, removed.newline)}`
|
|
409
|
+
: serializeTomlEntry(entry, removed.newline);
|
|
410
|
+
parseTomlRoot(updated, configPath);
|
|
411
|
+
return updated;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function removeTomlServerEntry(
|
|
415
|
+
content: string,
|
|
416
|
+
configPath: string
|
|
417
|
+
): { content: string; removed: boolean } {
|
|
418
|
+
const parsed = getTomlServerEntry(content, configPath);
|
|
419
|
+
if (!parsed.exists) {
|
|
420
|
+
return { content, removed: false };
|
|
421
|
+
}
|
|
422
|
+
const result = removeTomlGnoSections(content, configPath);
|
|
423
|
+
if (!result.removed) {
|
|
424
|
+
throw new CliError(
|
|
425
|
+
"RUNTIME",
|
|
426
|
+
`Unsupported inline GNO MCP entry in ${configPath}.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const updated = result.content ? `${result.content}${result.newline}` : "";
|
|
430
|
+
parseTomlRoot(updated, configPath);
|
|
431
|
+
return { content: updated, removed: true };
|
|
432
|
+
}
|