@xpufx/paseo-mcp-tools 0.1.3

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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/client/__snapshots__/attribution.test.tsx.snap +170 -0
  4. package/client/commands.ts +113 -0
  5. package/client/mcp-query.tsx +39 -0
  6. package/client/pill.tsx +886 -0
  7. package/client/timeline.tsx +136 -0
  8. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  9. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  10. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  11. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  12. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  13. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  14. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  15. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  16. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  17. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  18. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  19. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  20. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  21. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  22. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  23. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  24. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  25. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  26. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  27. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  28. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  29. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  31. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  32. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  33. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  34. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  35. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  36. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  37. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  38. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  39. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  40. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  41. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  42. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  43. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  44. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  45. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  46. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  47. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  48. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  49. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  50. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  51. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  52. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  53. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  54. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  55. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  56. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  57. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  58. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  59. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  60. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  61. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  62. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  63. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  64. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  65. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  66. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  67. package/docs/TEST_METHODOLOGY.md +29 -0
  68. package/package.json +41 -0
  69. package/paseo-plugin.json +4 -0
  70. package/scripts/version.mjs +4 -0
  71. package/server/discovery/extract.ts +277 -0
  72. package/server/discovery/types.ts +23 -0
  73. package/server/health/health.ts +274 -0
  74. package/server/inject.ts +32 -0
  75. package/server/mcp.ts +365 -0
  76. package/server/providers/README.md +51 -0
  77. package/server/providers/antigravity.ts +22 -0
  78. package/server/providers/catalog.ts +6 -0
  79. package/server/providers/claude.ts +21 -0
  80. package/server/providers/codex.ts +85 -0
  81. package/server/providers/family.ts +24 -0
  82. package/server/providers/index.ts +11 -0
  83. package/server/providers/opencode.ts +95 -0
  84. package/server/providers/paseo.ts +112 -0
  85. package/server/providers/pi.ts +39 -0
  86. package/server/settings.ts +19 -0
  87. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  88. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  89. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  90. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  91. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  92. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  93. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  94. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  95. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  96. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  97. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  98. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  99. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  100. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  101. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  102. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  103. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  104. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  105. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  106. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  107. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  108. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  109. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  110. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  111. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  112. package/shared/mcp.ts +207 -0
  113. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  114. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  115. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  116. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  117. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  118. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  119. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  120. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  121. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  122. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  123. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  124. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  125. package/shared/version.ts +2 -0
@@ -0,0 +1,149 @@
1
+ import type { HostToast } from "../host";
2
+ import { getOptionalClientHost } from "../host";
3
+
4
+ export interface CopyToClipboardOptions {
5
+ toast?: HostToast;
6
+ toastMessage?: string;
7
+ }
8
+
9
+ export type ClipboardTier = "navigator" | "host" | "rnAsync" | "rnSync" | "execCommand";
10
+
11
+ export interface ClipboardEnvironment {
12
+ hasNavigatorClipboard: boolean;
13
+ hasHostCopyText: boolean;
14
+ hasRnClipboard: boolean;
15
+ hasRnSetStringAsync: boolean;
16
+ isDom: boolean;
17
+ }
18
+
19
+ /**
20
+ * Deterministic tier order for an explicit copy.
21
+ *
22
+ * Web's `navigator.clipboard.writeText` is the only API that rejects when the
23
+ * clipboard did not change, so it leads. The synchronous
24
+ * `react-native-web` `Clipboard.setString` reports success even when its
25
+ * `document.execCommand("copy")` fails, which leaves the previous clipboard
26
+ * item in place while the UI claims success (xpufx-org/paseo#278); it is only
27
+ * usable off-DOM (native), where it is the real platform clipboard. In a DOM
28
+ * the checked `execCommand` fallback is preferred to that unverifiable path.
29
+ */
30
+ export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
31
+ const tiers: ClipboardTier[] = [];
32
+ if (env.hasNavigatorClipboard) tiers.push("navigator");
33
+ if (env.hasHostCopyText) tiers.push("host");
34
+ if (env.hasRnSetStringAsync) {
35
+ tiers.push("rnAsync");
36
+ } else if (env.hasRnClipboard && !env.isDom) {
37
+ tiers.push("rnSync");
38
+ }
39
+ tiers.push("execCommand");
40
+ return tiers;
41
+ }
42
+
43
+ /**
44
+ * Robust cross-platform clipboard copy helper for Paseo plugins.
45
+ * Works seamlessly across React Native (mobile), web, and desktop.
46
+ *
47
+ * Tier order comes from `clipboardTierOrder`; every tier reports failure
48
+ * honestly so a denied or blocked write never leaves the previous clipboard
49
+ * item behind under a fake success.
50
+ */
51
+ export async function copyToClipboard(
52
+ text: string,
53
+ options?: CopyToClipboardOptions,
54
+ ): Promise<boolean> {
55
+ if (text === null || text === undefined) return false;
56
+ const str = String(text);
57
+
58
+ const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
59
+
60
+ let rn: any;
61
+ try {
62
+ rn = require("react-native");
63
+ } catch {
64
+ // Ignore require error if not in RN context
65
+ }
66
+ const rnClipboard = rn?.Clipboard;
67
+ const rnSetStringAsync =
68
+ typeof rnClipboard?.setStringAsync === "function" ? rnClipboard.setStringAsync : undefined;
69
+ const rnSetString =
70
+ typeof rnClipboard?.setString === "function" ? rnClipboard.setString : undefined;
71
+ const copyText = getOptionalClientHost()?.copyText;
72
+
73
+ const isDom =
74
+ typeof globalObj.document !== "undefined" &&
75
+ typeof globalObj.document?.createElement === "function";
76
+
77
+ const tiers = clipboardTierOrder({
78
+ hasNavigatorClipboard: Boolean(globalObj.navigator?.clipboard?.writeText),
79
+ hasHostCopyText: Boolean(copyText),
80
+ hasRnClipboard: Boolean(rnSetStringAsync ?? rnSetString),
81
+ hasRnSetStringAsync: Boolean(rnSetStringAsync),
82
+ isDom,
83
+ });
84
+
85
+ let success = false;
86
+ for (const tier of tiers) {
87
+ if (success) break;
88
+ try {
89
+ switch (tier) {
90
+ case "navigator":
91
+ await globalObj.navigator.clipboard.writeText(str);
92
+ success = true;
93
+ break;
94
+ case "host":
95
+ if (copyText) {
96
+ await copyText(str);
97
+ success = true;
98
+ }
99
+ break;
100
+ case "rnAsync":
101
+ // Expo's async API returns a boolean; a rejected promise falls through.
102
+ success = (await rnSetStringAsync(str)) !== false;
103
+ break;
104
+ case "rnSync":
105
+ // Only reached off-DOM, where this is the native platform clipboard.
106
+ rnSetString(str);
107
+ success = true;
108
+ break;
109
+ case "execCommand": {
110
+ const doc = globalObj.document;
111
+ if (doc?.createElement && doc?.body) {
112
+ const textarea = doc.createElement("textarea");
113
+ textarea.value = str;
114
+ textarea.style.position = "fixed";
115
+ textarea.style.opacity = "0";
116
+ textarea.style.left = "-9999px";
117
+ doc.body.appendChild(textarea);
118
+ textarea.focus();
119
+ textarea.select();
120
+ const res = doc.execCommand("copy");
121
+ doc.body.removeChild(textarea);
122
+ success = res === true;
123
+ }
124
+ break;
125
+ }
126
+ }
127
+ } catch {
128
+ // Tier failed; try the next one.
129
+ }
130
+ }
131
+
132
+ if (success && options?.toast) {
133
+ try {
134
+ const toastAny = options.toast as any;
135
+ if (typeof toastAny.copied === "function") {
136
+ toastAny.copied(options.toastMessage);
137
+ } else if (typeof toastAny.show === "function") {
138
+ const msg = options.toastMessage
139
+ ? `Copied ${options.toastMessage} to clipboard`
140
+ : "Copied to clipboard";
141
+ toastAny.show(msg, { variant: "success" });
142
+ }
143
+ } catch {
144
+ // Ignore toast failure
145
+ }
146
+ }
147
+
148
+ return success;
149
+ }
@@ -0,0 +1,34 @@
1
+ export type HapticFeedbackType =
2
+ | "light"
3
+ | "medium"
4
+ | "heavy"
5
+ | "success"
6
+ | "warning"
7
+ | "error";
8
+
9
+ const PATTERNS: Record<HapticFeedbackType, number | number[]> = {
10
+ light: 10,
11
+ medium: 25,
12
+ heavy: 45,
13
+ success: [15, 40, 20],
14
+ warning: [30, 50, 30],
15
+ error: [40, 60, 40, 60, 40],
16
+ };
17
+
18
+ /**
19
+ * Cross-platform haptic feedback helper for Paseo plugins.
20
+ * Supports web vibration API and graceful fallback when vibration is unavailable.
21
+ */
22
+ export function triggerHaptic(type: HapticFeedbackType = "light"): boolean {
23
+ try {
24
+ const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
25
+ const nav = globalObj.navigator;
26
+ if (typeof nav?.vibrate === "function") {
27
+ const pattern = PATTERNS[type] ?? 15;
28
+ return Boolean(nav.vibrate(pattern));
29
+ }
30
+ } catch {
31
+ // Ignore environments where vibration permissions are denied or unavailable
32
+ }
33
+ return false;
34
+ }
@@ -0,0 +1,29 @@
1
+ # Testing Notes & Environment Log
2
+
3
+ ## Test Setup
4
+ - **Host**: Ubuntu 24.04 LTS (kernel 6.8, x86_64)
5
+ - **Runtime**: Node.js 22.23
6
+ - **Environment**: Local daemon process running on the host system
7
+
8
+ ---
9
+
10
+ ## What We Tested and How
11
+
12
+ ### 1. Adapter Crash Prevention & Contracts
13
+ We started by writing an automated test runner that loops over every registered adapter. For each one, we simulated a fresh machine with no config files or target software installed. The goal was to make sure no adapter throws an unhandled exception or kills the host process if its target isn't there. We also checked that every adapter returns the exact object shapes the UI expects, and that any API keys or tokens are stripped out before anything gets displayed.
14
+
15
+ ### 2. Config Discovery & File Precedence
16
+ Next, we tested how configs get discovered on disk. We created dummy folder structures with broken JSON, missing paths, and conflicting settings to verify that project-level configs override user-level or global ones. After synthetic tests passed, we ran the parser against real config files already sitting on the machine. This confirmed the parser handles real-world config formats without needing paid accounts or live subscriptions active.
17
+
18
+ ### 3. Transport Layers (Pipes & Local HTTP)
19
+ Once parsing worked, we tested the connections:
20
+ - **Subprocesses**: We checked that background processes spawn cleanly, talk over stdin/stdout pipes, and exit without leaving zombie processes behind.
21
+ - **Local HTTP**: We pointed health checks at a local daemon, timed the handshake, measured ping latency, and pulled the tool lists.
22
+
23
+ ### 4. Running Tools
24
+ With connections healthy, we ran real commands through the tool runner. We picked safe, read-only queries (like listing status or workspaces) and confirmed arguments pass through properly, the response text renders cleanly in the UI, and bad inputs return readable error messages instead of hanging.
25
+
26
+ ### 5. Packaging & Live Plugin Reload
27
+ Finally, we tested the build and installation flow:
28
+ - We verified that protocol libraries get bundled directly into the build so the plugin doesn't break on a machine without those packages installed globally.
29
+ - We installed the build into the running daemon, triggered a hot reload, watched the daemon logs to verify clean startup with no warnings, and clicked through the UI to verify everything loaded and ran.
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@xpufx/paseo-mcp-tools",
3
+ "private": false,
4
+ "version": "0.1.3",
5
+ "license": "MIT",
6
+ "scripts": {
7
+ "stamp": "node scripts/version.mjs",
8
+ "typecheck": "tsc --noEmit",
9
+ "test": "vitest run --passWithNoTests"
10
+ },
11
+ "devDependencies": {
12
+ "@getpaseo/plugin": "0.9.0-beta.2",
13
+ "@getpaseo/protocol": "0.9.0-beta.2",
14
+ "@tanstack/react-query": "^5.90.11",
15
+ "@types/node": "^22.10.0",
16
+ "@types/react": "^19.0.10",
17
+ "react": "19.1.0",
18
+ "react-native": "0.81.5",
19
+ "typescript": "^5.9.3",
20
+ "vitest": "^3.0.0",
21
+ "zod": "^4.4.3"
22
+ },
23
+ "files": [
24
+ "paseo-plugin.json",
25
+ "README.md",
26
+ "LICENSE",
27
+ "client",
28
+ "server",
29
+ "shared",
30
+ "docs",
31
+ "scripts",
32
+ "!**/*.test.ts",
33
+ "!**/*.test.tsx",
34
+ "!**/*.test.mjs",
35
+ "!**/.DS_Store"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/xpufx/paseo.git"
40
+ }
41
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "id": "mcp-tools",
3
+ "requirements": { "paseo": ">=0.8.0" }
4
+ }
@@ -0,0 +1,4 @@
1
+ const { stampVersion } = await import("../server/vendor/paseo-plugin-helper/version.ts");
2
+
3
+ const { version, updated } = stampVersion({ targetFile: "./shared/version.ts" });
4
+ console.log(`[build] PLUGIN_VERSION ${updated ? "stamped" : "up to date"}: "${version}" in shared/version.ts`);
@@ -0,0 +1,277 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { redactSecrets, tryParseJsonc } from "paseo-plugin-helper/server";
4
+ import type { McpServer, DiagnosticStep } from "./types";
5
+
6
+ export interface CandidatePath {
7
+ path: string;
8
+ label: string;
9
+ }
10
+
11
+ /**
12
+ * Redacts tokens, keys, passwords, and sensitive auth data via helper.
13
+ */
14
+ export function redact(text: string): string {
15
+ return redactSecrets(text, { mask: "•••" });
16
+ }
17
+
18
+ /**
19
+ * Universal JSON / JSONC / Trailing-comma tolerant parser via helper.
20
+ * Returns null instead of throwing so callers degrade gracefully.
21
+ */
22
+ export function parseJsonc(raw: string): unknown | null {
23
+ if (!raw || typeof raw !== "string") return null;
24
+ const parsed = tryParseJsonc<unknown>(raw, null);
25
+ if (parsed !== null) return parsed;
26
+ return tryParseJsonc<unknown>(raw.replace(/,\s*([}\]])/g, "$1"), null);
27
+ }
28
+
29
+ /**
30
+ * Heuristically finds the container holding MCP server definitions in arbitrary JSON.
31
+ */
32
+ function findServersContainer(data: unknown): Record<string, unknown> | Array<unknown> | null {
33
+ if (!data || typeof data !== "object") return null;
34
+
35
+ if (Array.isArray(data)) {
36
+ return data;
37
+ }
38
+
39
+ const obj = data as Record<string, unknown>;
40
+
41
+ // Common root key heuristics across all ecosystem tools
42
+ const candidateKeys = [
43
+ "mcpServers",
44
+ "mcp-servers",
45
+ "mcp_servers",
46
+ "servers",
47
+ "mcp",
48
+ "plugins",
49
+ ];
50
+
51
+ for (const key of candidateKeys) {
52
+ if (key in obj && obj[key] && typeof obj[key] === "object") {
53
+ return obj[key] as Record<string, unknown> | Array<unknown>;
54
+ }
55
+ }
56
+
57
+ // Heuristic: Is the root object itself a map of servers?
58
+ // Check if properties look like server definitions (have command, url, type, transport, args, or module)
59
+ const entries = Object.entries(obj);
60
+ if (entries.length > 0) {
61
+ const looksLikeServers = entries.every(([_, val]) => {
62
+ if (!val || typeof val !== "object" || Array.isArray(val)) return false;
63
+ const v = val as Record<string, unknown>;
64
+ return Boolean(v.command || v.url || v.type || v.transport || v.args || v.module || v.entrypoint);
65
+ });
66
+ if (looksLikeServers) {
67
+ return obj;
68
+ }
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Normalizes an arbitrary server definition entry into a standard McpServer.
76
+ */
77
+ export function normalizeMcpServer(
78
+ idPrefix: string,
79
+ nameHint: string,
80
+ defRaw: unknown,
81
+ sourcePath: string,
82
+ sourceLabel: string,
83
+ ): McpServer | null {
84
+ if (!defRaw || typeof defRaw !== "object" || Array.isArray(defRaw)) return null;
85
+ const def = defRaw as Record<string, unknown>;
86
+
87
+ // Check disabled heuristics
88
+ if (def.disabled === true || def.enabled === false || def.active === false) {
89
+ return null;
90
+ }
91
+
92
+ const name = typeof def.name === "string" && def.name.trim() ? def.name.trim() : nameHint;
93
+ if (!name) return null;
94
+
95
+ // Extract URL
96
+ const url =
97
+ typeof def.url === "string"
98
+ ? def.url
99
+ : typeof def.endpoint === "string"
100
+ ? def.endpoint
101
+ : typeof def.serverUrl === "string"
102
+ ? def.serverUrl
103
+ : null;
104
+
105
+ // Extract command & args. Keep the structured args alongside the
106
+ // joined command string so dialers can spawn the exact argv.
107
+ let command: string | null = null;
108
+ let argv: string[] | null = null;
109
+ if (typeof def.command === "string") {
110
+ const argList = Array.isArray(def.args) ? def.args.filter((a) => typeof a === "string") : [];
111
+ if (argList.length > 0) argv = argList;
112
+ const args = argList.join(" ");
113
+ command = args ? `${def.command} ${args}` : def.command;
114
+ } else if (typeof def.socket === "string") {
115
+ command = def.socket;
116
+ } else if (typeof def.module === "string") {
117
+ command = `node ${def.module}`;
118
+ }
119
+
120
+ // Determine transport
121
+ let transport: McpServer["transport"] = "unknown";
122
+ if (url) {
123
+ const isSse = def.type === "sse" || def.transport === "sse" || url.includes("/sse");
124
+ transport = isSse ? "sse" : "http";
125
+ } else if (command) {
126
+ transport = "stdio";
127
+ } else if (def.transport === "stdio" || def.transport === "http" || def.transport === "sse") {
128
+ transport = def.transport;
129
+ }
130
+
131
+ // Detect secrets
132
+ const hasSecrets = Boolean(
133
+ def.env ||
134
+ def.headers ||
135
+ def.auth ||
136
+ def.bearerToken ||
137
+ def.apiKey ||
138
+ def.token ||
139
+ def.secret
140
+ );
141
+
142
+ // Description heuristic
143
+ const description =
144
+ (typeof def.description === "string" && def.description) ||
145
+ url ||
146
+ command ||
147
+ "";
148
+
149
+ return {
150
+ id: `session:${idPrefix}:${name}`,
151
+ name,
152
+ transport,
153
+ source: {
154
+ kind: "session",
155
+ label: sourceLabel,
156
+ path: sourcePath,
157
+ },
158
+ command,
159
+ args: argv,
160
+ url,
161
+ description,
162
+ hasSecrets,
163
+ configPreview: JSON.stringify(redactSecrets(def, { mask: "•••" }), null, 2),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Extracts all valid MCP servers from raw JSON/JSONC text using heuristics.
169
+ */
170
+ export function extractMcpServersFromText(
171
+ rawText: string,
172
+ idPrefix: string,
173
+ sourcePath: string,
174
+ sourceLabel: string,
175
+ ): McpServer[] {
176
+ const parsed = parseJsonc(rawText);
177
+ if (!parsed) return [];
178
+
179
+ const container = findServersContainer(parsed);
180
+ if (!container) return [];
181
+
182
+ const servers: McpServer[] = [];
183
+
184
+ if (Array.isArray(container)) {
185
+ for (let i = 0; i < container.length; i++) {
186
+ const item = container[i];
187
+ const s = normalizeMcpServer(idPrefix, `server-${i + 1}`, item, sourcePath, sourceLabel);
188
+ if (s) servers.push(s);
189
+ }
190
+ } else {
191
+ for (const [name, item] of Object.entries(container)) {
192
+ const s = normalizeMcpServer(idPrefix, name, item, sourcePath, sourceLabel);
193
+ if (s) servers.push(s);
194
+ }
195
+ }
196
+
197
+ return servers;
198
+ }
199
+
200
+ /**
201
+ * Discovers MCP servers across multiple candidate paths with low -> high precedence merging.
202
+ */
203
+ export async function discoverFromCandidates(
204
+ idPrefix: string,
205
+ candidates: CandidatePath[],
206
+ ): Promise<{ servers: McpServer[]; error: string | null; steps: DiagnosticStep[] }> {
207
+ const merged = new Map<string, { def: unknown; name: string; source: CandidatePath }>();
208
+ const steps: DiagnosticStep[] = [];
209
+
210
+ for (const cand of candidates) {
211
+ if (!existsSync(cand.path)) {
212
+ steps.push({
213
+ target: cand.path,
214
+ status: "missing",
215
+ details: `${cand.label} does not exist`,
216
+ contentPreview: null,
217
+ });
218
+ continue;
219
+ }
220
+ try {
221
+ const raw = await readFile(cand.path, "utf8");
222
+ const parsed = parseJsonc(raw);
223
+ if (!parsed) {
224
+ steps.push({
225
+ target: cand.path,
226
+ status: "error",
227
+ details: `${cand.label} (${raw.length} bytes) · Invalid JSON/JSONC syntax`,
228
+ contentPreview: redact(raw.slice(0, 1000)),
229
+ });
230
+ continue;
231
+ }
232
+
233
+ const container = findServersContainer(parsed);
234
+ let count = 0;
235
+ if (container) {
236
+ if (Array.isArray(container)) {
237
+ count = container.length;
238
+ for (let i = 0; i < container.length; i++) {
239
+ const item = container[i];
240
+ const name = (item && typeof item === "object" && typeof (item as Record<string, unknown>).name === "string")
241
+ ? (item as Record<string, unknown>).name as string
242
+ : `server-${i + 1}`;
243
+ merged.set(name, { def: item, name, source: cand });
244
+ }
245
+ } else {
246
+ const entries = Object.entries(container);
247
+ count = entries.length;
248
+ for (const [name, item] of entries) {
249
+ merged.set(name, { def: item, name, source: cand });
250
+ }
251
+ }
252
+ }
253
+
254
+ steps.push({
255
+ target: cand.path,
256
+ status: "found",
257
+ details: `${cand.label} (${raw.length} bytes) · Valid config with ${count} MCP server(s)`,
258
+ contentPreview: redact(raw.slice(0, 1000)),
259
+ });
260
+ } catch (e) {
261
+ steps.push({
262
+ target: cand.path,
263
+ status: "error",
264
+ details: `${cand.label} failed to read: ${e instanceof Error ? e.message : String(e)}`,
265
+ contentPreview: null,
266
+ });
267
+ }
268
+ }
269
+
270
+ const servers: McpServer[] = [];
271
+ for (const [_, { def, name, source }] of merged) {
272
+ const s = normalizeMcpServer(idPrefix, name, def, source.path, source.label);
273
+ if (s) servers.push(s);
274
+ }
275
+
276
+ return { servers, error: null, steps };
277
+ }
@@ -0,0 +1,23 @@
1
+ import type { McpServer, DiagnosticStep } from "../../shared/mcp";
2
+
3
+ export type { McpServer, DiagnosticStep };
4
+
5
+ export interface ProbeContext {
6
+ agentId: string;
7
+ provider: string;
8
+ cwd: string;
9
+ sessionId?: string | null;
10
+ }
11
+
12
+ export interface ProbeResult {
13
+ servers: McpServer[];
14
+ error?: string | null;
15
+ steps?: DiagnosticStep[];
16
+ }
17
+
18
+ export interface McpProbe {
19
+ id: string;
20
+ label: string;
21
+ matches(provider: string): boolean;
22
+ probe(ctx: ProbeContext): Promise<ProbeResult>;
23
+ }