@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,886 @@
1
+ import { useRpc } from "@getpaseo/plugin/client";
2
+ import { Icon, useToast, ScrollView, FlatList, TextInput as HostTextInput, copyText } from "@getpaseo/plugin/client/react-native";
3
+ import {
4
+ initClientHelpers,
5
+ type ComposerPillRegistrar,
6
+ } from "paseo-plugin-helper/client";
7
+
8
+ initClientHelpers({ Icon, Modal, useRpc, useToast, copyText, ScrollView, FlatList, TextInput: HostTextInput });
9
+ import { useMemo, useState } from "react";
10
+ import { ActivityIndicator, Pressable, Text, View } from "react-native";
11
+ import { Modal } from "@getpaseo/plugin/client/react-native";
12
+ import {
13
+ AboutSection,
14
+ ActionBar,
15
+ Badge,
16
+ Button,
17
+ Card,
18
+ CodeBlock,
19
+ EmptyState,
20
+ FormRow,
21
+ ModalBody,
22
+ PluginThemeProvider,
23
+ SearchInput,
24
+ StatusDot,
25
+ Tabs,
26
+ TextInput,
27
+ Toggle,
28
+ copyToClipboard,
29
+ registerComposerPill,
30
+ triggerHaptic,
31
+ usePluginSettings,
32
+ usePluginTheme,
33
+ useResponsive,
34
+ type RenderModalProps,
35
+ type RenderPillProps,
36
+ type TabItem,
37
+ } from "paseo-plugin-helper/client";
38
+ import { useMcpHealthQuery, useMcpQuery } from "./mcp-query";
39
+ import {
40
+ callMcpTool,
41
+ diagnoseMcp,
42
+ mcpToolsSettingsContract,
43
+ PLUGIN_ATTRIBUTION,
44
+ readMcp,
45
+ type ToolInfo,
46
+ } from "../shared/mcp";
47
+ import { PLUGIN_VERSION } from "../shared/version";
48
+
49
+ type HealthInfo = {
50
+ serverId: string;
51
+ name: string;
52
+ status: "healthy" | "degraded" | "down" | "unknown";
53
+ latencyMs: number;
54
+ toolCount: number | null;
55
+ tools: string[] | null;
56
+ toolDetails?: ToolInfo[] | null;
57
+ instructions: string | null;
58
+ error: string | null;
59
+ };
60
+
61
+ const TABS: TabItem[] = [
62
+ { id: "servers", label: "Servers", shortLabel: "Servers", icon: "Plug" },
63
+ { id: "diagnostics", label: "Diagnostics", shortLabel: "Diag", icon: "Activity" },
64
+ { id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
65
+ { id: "about", label: "About", shortLabel: "About", icon: "Info" },
66
+ ];
67
+
68
+ function McpPillBody(props: RenderPillProps) {
69
+ const { colors } = usePluginTheme();
70
+ const { isCompact } = useResponsive();
71
+ const { data } = useMcpQuery(props.agentId);
72
+ const n = data?.servers.length ?? 0;
73
+
74
+ return (
75
+ <View
76
+ accessibilityLabel={`MCP ${n > 0 ? n : ""} ${PLUGIN_ATTRIBUTION}`.trim()}
77
+ style={{ flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 6 }}
78
+ >
79
+ <StatusDot variant={n > 0 ? "success" : "neutral"} />
80
+ <Text numberOfLines={1} style={{ fontSize: 11, color: colors.foregroundMuted, flexShrink: 1 }}>
81
+ {isCompact ? (n > 0 ? `${n}` : "MCP") : n > 0 ? `MCP ${n}` : "MCP"}
82
+ </Text>
83
+ </View>
84
+ );
85
+ }
86
+
87
+ function McpModalContent({ agentId, close, theme, layout }: RenderModalProps) {
88
+ const { colors } = usePluginTheme();
89
+ const toast = useToast();
90
+ const [activeTab, setActiveTab] = useState("servers");
91
+ const { settings, updateSettings, resetSettings, isUpdating } =
92
+ usePluginSettings(mcpToolsSettingsContract);
93
+
94
+ const query = useMcpQuery(agentId);
95
+ const callRead = useRpc(readMcp);
96
+ const callToolRpc = useRpc(callMcpTool);
97
+ const callDiagnose = useRpc(diagnoseMcp);
98
+
99
+ const [modalOpen] = useState(true);
100
+ const healthQuery = useMcpHealthQuery(agentId, undefined, {
101
+ isOpen: modalOpen,
102
+ rate: settings.healthPollingRate,
103
+ });
104
+
105
+ const [search, setSearch] = useState("");
106
+ const [selected, setSelected] = useState<string | null>(null);
107
+ const [detail, setDetail] = useState<{ raw: string; redacted: string; path: string } | null>(null);
108
+ const [loadingDetail, setLoadingDetail] = useState(false);
109
+ const [activeTool, setActiveTool] = useState<ToolInfo | null>(null);
110
+ const [toolArgs, setToolArgs] = useState<Record<string, string>>({});
111
+ const [toolExecuting, setToolExecuting] = useState(false);
112
+ const [toolResult, setToolResult] = useState<{
113
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>;
114
+ isError?: boolean;
115
+ } | null>(null);
116
+ const [toolSearch, setToolSearch] = useState("");
117
+
118
+ const [diagnosticsLoading, setDiagnosticsLoading] = useState(false);
119
+ const [diagnosticData, setDiagnosticData] = useState<{
120
+ provider: string;
121
+ cwd: string;
122
+ probeId: string | null;
123
+ probeLabel: string | null;
124
+ steps: Array<{
125
+ target: string;
126
+ status: "found" | "missing" | "error" | "skipped";
127
+ details: string;
128
+ contentPreview: string | null;
129
+ }>;
130
+ discoveredServerCount: number;
131
+ error: string | null;
132
+ } | null>(null);
133
+
134
+ const copy = (value: string, label = "Value") =>
135
+ copyToClipboard(value, { toast, toastMessage: label });
136
+
137
+ const healthMap = useMemo(() => {
138
+ const map = new Map<string, HealthInfo>();
139
+ for (const r of healthQuery.data?.results ?? []) {
140
+ map.set(r.serverId, r as HealthInfo);
141
+ map.set(r.name, r as HealthInfo);
142
+ }
143
+ return map;
144
+ }, [healthQuery.data]);
145
+
146
+ const health = selected ? healthMap.get(selected) ?? null : null;
147
+ const healthLoading = healthQuery.isFetching && !health;
148
+
149
+ const runDiagnostics = async () => {
150
+ setDiagnosticsLoading(true);
151
+ try {
152
+ const data = await callDiagnose({ agentId });
153
+ setDiagnosticData(data);
154
+ } catch (e) {
155
+ toast.error(e instanceof Error ? e.message : String(e));
156
+ } finally {
157
+ setDiagnosticsLoading(false);
158
+ }
159
+ };
160
+
161
+ const term = search.trim().toLowerCase();
162
+ const matches = (s: { name: string; description: string }) =>
163
+ !term || s.name.toLowerCase().includes(term) || s.description.toLowerCase().includes(term);
164
+
165
+ const servers = (query.data?.servers ?? []).filter(matches);
166
+
167
+ const groupedServers = useMemo(() => {
168
+ const m = new Map<string, typeof servers>();
169
+ for (const s of servers) {
170
+ const k = s.source.label;
171
+ if (!m.has(k)) m.set(k, []);
172
+ m.get(k)!.push(s);
173
+ }
174
+ return [...m.entries()];
175
+ }, [servers]);
176
+
177
+ const filteredTools = useMemo(() => {
178
+ if (!health?.tools) return [];
179
+ if (!toolSearch.trim()) return health.tools;
180
+ const q = toolSearch.toLowerCase().trim();
181
+ return health.tools.filter((toolName) => {
182
+ if (toolName.toLowerCase().includes(q)) return true;
183
+ const details = health.toolDetails?.find((d) => d.name === toolName);
184
+ if (details?.description?.toLowerCase().includes(q)) return true;
185
+ return false;
186
+ });
187
+ }, [health?.tools, health?.toolDetails, toolSearch]);
188
+
189
+ const resetExecutionState = () => {
190
+ setActiveTool(null);
191
+ setToolArgs({});
192
+ setToolResult(null);
193
+ };
194
+
195
+ const openDetail = async (id: string) => {
196
+ resetExecutionState();
197
+ setSelected(id);
198
+ setToolSearch("");
199
+ setLoadingDetail(true);
200
+ try {
201
+ const d = await callRead({ agentId, serverId: id });
202
+ setDetail({ raw: d.raw, redacted: d.redacted, path: d.path });
203
+ } catch (e) {
204
+ toast.error(e instanceof Error ? e.message : String(e));
205
+ setSelected(null);
206
+ } finally {
207
+ setLoadingDetail(false);
208
+ }
209
+ };
210
+
211
+ const backToList = () => {
212
+ setSelected(null);
213
+ setDetail(null);
214
+ setToolSearch("");
215
+ resetExecutionState();
216
+ };
217
+
218
+ const openToolRunner = (toolName: string) => {
219
+ triggerHaptic("light");
220
+ const details = health?.toolDetails?.find((d) => d.name === toolName);
221
+ const toolObj: ToolInfo = details ?? { name: toolName };
222
+ setActiveTool(toolObj);
223
+ const initialArgs: Record<string, string> = {};
224
+ if (toolObj.inputSchema?.properties) {
225
+ for (const [key, prop] of Object.entries(toolObj.inputSchema.properties)) {
226
+ if (prop.default !== undefined) {
227
+ initialArgs[key] = typeof prop.default === "object" ? JSON.stringify(prop.default) : String(prop.default);
228
+ } else {
229
+ initialArgs[key] = "";
230
+ }
231
+ }
232
+ }
233
+ setToolArgs(initialArgs);
234
+ setToolResult(null);
235
+ };
236
+
237
+ const handleExecuteTool = async () => {
238
+ if (!activeTool || !selected) return;
239
+
240
+ const required = activeTool.inputSchema?.required ?? [];
241
+ const missing = required.filter((req) => !toolArgs[req]?.trim());
242
+ if (missing.length > 0) {
243
+ toast.error(`Missing required parameter(s): ${missing.join(", ")}`);
244
+ return;
245
+ }
246
+
247
+ const parsedArgs: Record<string, unknown> = {};
248
+ const props = activeTool.inputSchema?.properties ?? {};
249
+ for (const [key, val] of Object.entries(toolArgs)) {
250
+ if (!val && !required.includes(key)) continue;
251
+ const typeRaw = props[key]?.type;
252
+ const type = Array.isArray(typeRaw) ? typeRaw.find((t) => t !== "null") ?? typeRaw[0] : typeRaw;
253
+ if (type === "number" || type === "integer") {
254
+ const n = Number(val);
255
+ parsedArgs[key] = isNaN(n) ? val : n;
256
+ } else if (type === "boolean") {
257
+ parsedArgs[key] = val.toLowerCase() === "true" || val === "1";
258
+ } else if (type === "array" || type === "object") {
259
+ try {
260
+ parsedArgs[key] = JSON.parse(val);
261
+ } catch {
262
+ parsedArgs[key] = val;
263
+ }
264
+ } else {
265
+ parsedArgs[key] = val;
266
+ }
267
+ }
268
+
269
+ setToolExecuting(true);
270
+ try {
271
+ triggerHaptic("medium");
272
+ const res = await callToolRpc({
273
+ agentId,
274
+ serverId: selected,
275
+ toolName: activeTool.name,
276
+ arguments: parsedArgs,
277
+ });
278
+ setToolResult(res);
279
+ if (res.isError) {
280
+ triggerHaptic("error");
281
+ toast.error("Tool returned an error");
282
+ } else {
283
+ triggerHaptic("success");
284
+ toast.show("Tool executed successfully");
285
+ }
286
+ } catch (e) {
287
+ triggerHaptic("error");
288
+ setToolResult({
289
+ content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
290
+ isError: true,
291
+ });
292
+ toast.error(e instanceof Error ? e.message : String(e));
293
+ } finally {
294
+ setToolExecuting(false);
295
+ }
296
+ };
297
+
298
+ const statusVariant = (status?: string) =>
299
+ status === "healthy" ? "success" as const
300
+ : status === "degraded" ? "warning" as const
301
+ : status === "down" ? "danger" as const
302
+ : "neutral" as const;
303
+
304
+ const lastCheck = healthQuery.dataUpdatedAt
305
+ ? new Date(healthQuery.dataUpdatedAt).toLocaleTimeString()
306
+ : null;
307
+
308
+ const renderServers = () => {
309
+ if (selected) {
310
+ const server = servers.find((s) => s.id === selected);
311
+ if (activeTool) {
312
+ return (
313
+ <View key={`runner:${selected}:${activeTool.name}`} style={{ gap: 12 }}>
314
+ <Button
315
+ label="← Back to server"
316
+ variant="ghost"
317
+ size="sm"
318
+ onPress={() => {
319
+ resetExecutionState();
320
+ }}
321
+ />
322
+ <Card>
323
+ <Card.Header title={activeTool.name} icon="Play" />
324
+ {activeTool.description ? (
325
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, lineHeight: 16 }}>
326
+ {activeTool.description}
327
+ </Text>
328
+ ) : null}
329
+ </Card>
330
+
331
+ <Card>
332
+ <Card.Header
333
+ title={`Parameters (${activeTool.inputSchema?.properties ? Object.keys(activeTool.inputSchema.properties).length : 0})`}
334
+ />
335
+ {activeTool.inputSchema?.properties && Object.keys(activeTool.inputSchema.properties).length > 0 ? (
336
+ <View style={{ gap: 10 }}>
337
+ {Object.entries(activeTool.inputSchema.properties).map(([paramName, prop]) => {
338
+ const isRequired = (activeTool.inputSchema?.required ?? []).includes(paramName);
339
+ const typeRaw = prop.type;
340
+ const typeLabel = Array.isArray(typeRaw) ? typeRaw.join(" | ") : typeRaw;
341
+ const isStructured =
342
+ typeRaw === "object" || typeRaw === "array" ||
343
+ (Array.isArray(typeRaw) && (typeRaw.includes("object") || typeRaw.includes("array")));
344
+ return (
345
+ <TextInput
346
+ key={paramName}
347
+ label={`${paramName}${isRequired ? " *" : ""}${typeLabel ? ` (${typeLabel})` : ""}`}
348
+ helperText={prop.description}
349
+ errorText={isRequired && !(toolArgs[paramName]?.trim()) ? "Required" : undefined}
350
+ value={toolArgs[paramName] ?? ""}
351
+ onChangeText={(text) => setToolArgs((prev) => ({ ...prev, [paramName]: text }))}
352
+ placeholder={prop.default !== undefined ? String(prop.default) : isRequired ? "Required value..." : "Optional value..."}
353
+ autoCapitalize="none"
354
+ autoCorrect={false}
355
+ multiline={isStructured}
356
+ mono={isStructured}
357
+ />
358
+ );
359
+ })}
360
+ </View>
361
+ ) : (
362
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, fontStyle: "italic" }}>
363
+ This tool takes no parameters.
364
+ </Text>
365
+ )}
366
+ <Button
367
+ label={toolExecuting ? "Executing tool…" : "Execute Tool"}
368
+ variant="primary"
369
+ icon="Play"
370
+ loading={toolExecuting}
371
+ disabled={toolExecuting}
372
+ onPress={() => void handleExecuteTool()}
373
+ />
374
+ </Card>
375
+
376
+ {toolResult ? (
377
+ <Card>
378
+ <Card.Header
379
+ title={toolResult.isError ? "✕ Execution Failed" : "✓ Result"}
380
+ badge={
381
+ <Badge
382
+ label={toolResult.isError ? "error" : "ok"}
383
+ variant={toolResult.isError ? "danger" : "success"}
384
+ />
385
+ }
386
+ />
387
+ <CodeBlock
388
+ language="json"
389
+ code={toolResult.content.map((c) => c.text ?? JSON.stringify(c, null, 2)).join("\n\n")}
390
+ copyable
391
+ />
392
+ </Card>
393
+ ) : null}
394
+ </View>
395
+ );
396
+ }
397
+ if (loadingDetail) {
398
+ return (
399
+ <View style={{ padding: 24, alignItems: "center" }}>
400
+ <ActivityIndicator color={colors.foregroundMuted} />
401
+ </View>
402
+ );
403
+ }
404
+ if (!detail) {
405
+ return <EmptyState icon="Plug" title="No detail" description="Could not load server detail." />;
406
+ }
407
+ return (
408
+ <View style={{ gap: 12 }}>
409
+ <Button label="← Back to list" variant="ghost" size="sm" onPress={backToList} />
410
+ <Card>
411
+ <Card.Header
412
+ title={server?.name ?? selected}
413
+ badge={server ? <Badge label={server.transport} variant="neutral" /> : undefined}
414
+ />
415
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11, fontFamily: "monospace" }}>
416
+ {detail.path}
417
+ </Text>
418
+ {healthLoading ? (
419
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
420
+ <ActivityIndicator size="small" color={colors.foregroundMuted} />
421
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11 }}>Checking health…</Text>
422
+ </View>
423
+ ) : health ? (
424
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
425
+ <StatusDot variant={statusVariant(health.status)} />
426
+ <Text style={{ color: colors.foreground, fontSize: 13, fontWeight: "700" }}>
427
+ {health.latencyMs}ms · {health.toolCount ?? "?"} tools
428
+ </Text>
429
+ {health.error ? (
430
+ <Text style={{ color: colors.statusDanger, fontSize: 11 }}>{health.error.slice(0, 120)}</Text>
431
+ ) : null}
432
+ </View>
433
+ ) : null}
434
+ </Card>
435
+
436
+ {health?.instructions ? (
437
+ <Card>
438
+ <Card.Header title="Instructions" />
439
+ <Text selectable style={{ color: colors.foreground, fontSize: 12, lineHeight: 18 }}>
440
+ {health.instructions}
441
+ </Text>
442
+ </Card>
443
+ ) : null}
444
+
445
+ {health?.tools && health.tools.length > 0 ? (
446
+ <Card>
447
+ <Card.Header
448
+ title={`Available Tools (${filteredTools.length}${filteredTools.length !== health.tools.length ? ` of ${health.tools.length}` : ""}) — tap to run`}
449
+ />
450
+ <SearchInput
451
+ value={toolSearch}
452
+ onChangeText={setToolSearch}
453
+ placeholder="Filter tools by name or description..."
454
+ />
455
+ {filteredTools.length === 0 ? (
456
+ <EmptyState
457
+ icon="Search"
458
+ title="No tools match"
459
+ description={`Nothing matches "${toolSearch}"`}
460
+ />
461
+ ) : (
462
+ <View style={{ gap: 6 }}>
463
+ {filteredTools.map((toolName) => {
464
+ const details = health?.toolDetails?.find((d) => d.name === toolName);
465
+ return (
466
+ <Pressable
467
+ key={toolName}
468
+ onPress={() => openToolRunner(toolName)}
469
+ style={{
470
+ flexDirection: "row",
471
+ alignItems: "center",
472
+ gap: 8,
473
+ padding: 8,
474
+ borderRadius: 6,
475
+ backgroundColor: colors.foregroundMuted + "10",
476
+ borderWidth: 1,
477
+ borderColor: colors.foregroundMuted + "18",
478
+ }}
479
+ >
480
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
481
+ <Text
482
+ numberOfLines={1}
483
+ style={{ color: colors.foreground, fontSize: 12, fontWeight: "600", fontFamily: "monospace", textAlign: "left" }}
484
+ >
485
+ {toolName}
486
+ </Text>
487
+ {details?.description ? (
488
+ <Text numberOfLines={2} style={{ color: colors.foregroundMuted, fontSize: 11, textAlign: "left" }}>
489
+ {details.description}
490
+ </Text>
491
+ ) : null}
492
+ </View>
493
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 6, paddingVertical: 3, borderRadius: 4, backgroundColor: colors.accent + "20", flexShrink: 0 }}>
494
+ <Icon name="Play" size={10} color={colors.accent} />
495
+ <Text style={{ color: colors.accent, fontSize: 11, fontWeight: "600" }}>Run</Text>
496
+ </View>
497
+ </Pressable>
498
+ );
499
+ })}
500
+ </View>
501
+ )}
502
+ </Card>
503
+ ) : null}
504
+
505
+ <Card>
506
+ <Card.Header title="Raw Config (redacted)" />
507
+ <CodeBlock language="json" code={detail.redacted} maxHeight={240} copyable />
508
+ </Card>
509
+ </View>
510
+ );
511
+ }
512
+
513
+ return (
514
+ <View style={{ gap: 12 }}>
515
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
516
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11, flex: 1 }} numberOfLines={1}>
517
+ {lastCheck ? `Last check ${lastCheck}` : "Never checked"}
518
+ {query.data?.provider ? ` · ${query.data.provider}` : ""}
519
+ {` · ${PLUGIN_VERSION}`}
520
+ {query.isFetching ? " • checking…" : ""}
521
+ </Text>
522
+ <ActionBar align="flex-end">
523
+ <Button
524
+ label="Diagnose"
525
+ variant="ghost"
526
+ size="sm"
527
+ icon="Activity"
528
+ onPress={() => {
529
+ triggerHaptic("light");
530
+ setActiveTab("diagnostics");
531
+ void runDiagnostics();
532
+ }}
533
+ />
534
+ <Button
535
+ label="Refresh"
536
+ variant="ghost"
537
+ size="sm"
538
+ icon="RefreshCw"
539
+ loading={query.isFetching}
540
+ disabled={query.isFetching}
541
+ onPress={() => void query.refetch()}
542
+ />
543
+ </ActionBar>
544
+ </View>
545
+ <SearchInput
546
+ value={search}
547
+ onChangeText={setSearch}
548
+ placeholder="Search servers & tools"
549
+ />
550
+ {query.isPending ? (
551
+ <ActivityIndicator color={colors.foregroundMuted} />
552
+ ) : query.isError ? (
553
+ <Text style={{ color: colors.statusDanger }}>{(query.error as Error).message}</Text>
554
+ ) : servers.length === 0 ? (
555
+ <EmptyState
556
+ icon="Plug"
557
+ title={term ? "No matches" : "No MCP servers found"}
558
+ description={term ? `Nothing matches "${term}"` : "No MCP servers discovered for this agent session."}
559
+ />
560
+ ) : (
561
+ <View>
562
+ {groupedServers.map(([label, items]) => (
563
+ <View key={label} style={{ marginTop: 12 }}>
564
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11, textTransform: "uppercase", marginBottom: 6 }}>
565
+ {label}
566
+ </Text>
567
+ {items.map((s) => {
568
+ const h = healthMap.get(s.id) ?? healthMap.get(s.name);
569
+ return (
570
+ <Pressable
571
+ key={s.id}
572
+ onPress={() => {
573
+ triggerHaptic("light");
574
+ void openDetail(s.id);
575
+ }}
576
+ style={{ paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: colors.foregroundMuted + "18" }}
577
+ >
578
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 6 }}>
579
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, flex: 1, minWidth: 0 }}>
580
+ <StatusDot variant={statusVariant(h?.status)} size="sm" />
581
+ <Text numberOfLines={1} style={{ color: colors.foreground, fontWeight: "600", flexShrink: 1 }}>
582
+ {s.name}
583
+ </Text>
584
+ <Badge label={s.transport} variant="neutral" />
585
+ {s.hasSecrets ? <Icon name="KeyRound" size={12} color={colors.foregroundMuted} /> : null}
586
+ </View>
587
+ {h ? (
588
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11, flexShrink: 0 }}>
589
+ {h.latencyMs}ms{h.toolCount !== null ? ` · ${h.toolCount} tools` : ""}
590
+ </Text>
591
+ ) : healthQuery.isFetching ? (
592
+ <ActivityIndicator size="small" color={colors.foregroundMuted} />
593
+ ) : null}
594
+ </View>
595
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12, marginTop: 2 }} numberOfLines={2}>
596
+ {s.description || s.command || s.url || "—"}
597
+ </Text>
598
+ </Pressable>
599
+ );
600
+ })}
601
+ </View>
602
+ ))}
603
+ {query.data?.cwd ? (
604
+ <Text style={{ color: colors.foregroundMuted, fontSize: 10, marginTop: 12, fontFamily: "monospace" }}>
605
+ {query.data.cwd}
606
+ </Text>
607
+ ) : null}
608
+ {query.data?.error ? (
609
+ <Text style={{ color: colors.statusDanger, fontSize: 11, marginTop: 6 }}>{query.data.error}</Text>
610
+ ) : null}
611
+ </View>
612
+ )}
613
+ </View>
614
+ );
615
+ };
616
+
617
+ const renderDiagnostics = () => (
618
+ <View style={{ gap: 12 }}>
619
+ <ActionBar align="flex-end">
620
+ <Button
621
+ label="Re-run"
622
+ variant="ghost"
623
+ size="sm"
624
+ icon="Activity"
625
+ loading={diagnosticsLoading}
626
+ disabled={diagnosticsLoading}
627
+ onPress={() => void runDiagnostics()}
628
+ />
629
+ </ActionBar>
630
+ {diagnosticsLoading ? (
631
+ <View style={{ padding: 24, alignItems: "center", gap: 8 }}>
632
+ <ActivityIndicator color={colors.accent} />
633
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12 }}>Running host diagnostics…</Text>
634
+ </View>
635
+ ) : diagnosticData ? (
636
+ <View style={{ gap: 12 }}>
637
+ <Card>
638
+ <Card.Header
639
+ title={`Probe Diagnostic Report (${PLUGIN_VERSION})`}
640
+ badge={<Badge label={`${diagnosticData.discoveredServerCount} servers`} variant="success" />}
641
+ />
642
+ <Text style={{ color: colors.foregroundMuted, fontSize: 12 }}>
643
+ Provider: {diagnosticData.provider}
644
+ {diagnosticData.probeLabel ? ` (${diagnosticData.probeLabel})` : " (no matching probe in registry)"}
645
+ </Text>
646
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11, fontFamily: "monospace" }}>
647
+ CWD: {diagnosticData.cwd}
648
+ </Text>
649
+ {diagnosticData.error ? (
650
+ <Text style={{ color: colors.statusDanger, fontSize: 12, marginTop: 4 }}>
651
+ Probe Error: {diagnosticData.error}
652
+ </Text>
653
+ ) : null}
654
+ <CodeBlock language="json" code={JSON.stringify(diagnosticData, null, 2)} maxHeight={200} copyable />
655
+ </Card>
656
+
657
+ {diagnosticData.steps.map((step, idx) => (
658
+ <Card key={idx}>
659
+ <Card.Header
660
+ title={step.target}
661
+ badge={<Badge label={step.status} variant={statusVariant(step.status === "found" ? "healthy" : step.status === "error" ? "down" : "unknown")} />}
662
+ />
663
+ <Text style={{ color: colors.foregroundMuted, fontSize: 11, marginTop: 2 }}>{step.details}</Text>
664
+ {step.contentPreview ? (
665
+ <CodeBlock language="json" code={step.contentPreview} maxHeight={160} copyable />
666
+ ) : null}
667
+ </Card>
668
+ ))}
669
+ </View>
670
+ ) : (
671
+ <EmptyState
672
+ icon="Activity"
673
+ title="No diagnostics yet"
674
+ description="Run host diagnostics to inspect provider MCP probes."
675
+ actionLabel="Run Diagnostics" onAction={() => void runDiagnostics()}
676
+ />
677
+ )}
678
+ </View>
679
+ );
680
+
681
+ const renderSettings = () => (
682
+ <View style={{ gap: 12 }}>
683
+ <Card>
684
+ <Card.Header title="Health Polling" subtitle="Background refresh rate for server health" />
685
+ <FormRow label="Timeline digest rows" description="Append a health digest row to the agent timeline after each check">
686
+ <Toggle
687
+ value={settings.healthDigestRows}
688
+ onValueChange={(val) => {
689
+ triggerHaptic("light");
690
+ updateSettings({ healthDigestRows: val });
691
+ }}
692
+ />
693
+ </FormRow>
694
+ <FormRow label="Polling rate" description="Paused disables background health polling">
695
+ <View style={{ flexDirection: "row", gap: 6, flexWrap: "wrap" }}>
696
+ {(["1s", "2s", "5s", "10s", "15s", "30s", "60s", "5m", "paused"] as const).map((r) => (
697
+ <Button
698
+ key={r}
699
+ label={r}
700
+ size="sm"
701
+ variant={settings.healthPollingRate === r ? "primary" : "ghost"}
702
+ onPress={() => {
703
+ triggerHaptic("light");
704
+ updateSettings({ healthPollingRate: r });
705
+ }}
706
+ />
707
+ ))}
708
+ </View>
709
+ </FormRow>
710
+ </Card>
711
+
712
+ <Card>
713
+ <Card.Header title="Gateway Injection" subtitle="Auto-register the gateway on agent.create" />
714
+ <FormRow label="Inject gateway" description="Add the gateway MCP server to every new agent">
715
+ <Toggle
716
+ value={settings.gatewayInject}
717
+ onValueChange={(val) => {
718
+ triggerHaptic("light");
719
+ updateSettings({ gatewayInject: val });
720
+ }}
721
+ />
722
+ </FormRow>
723
+ <FormRow label="Gateway endpoint" description="HTTP URL injected into new agents">
724
+ <TextInput
725
+ value={settings.gatewayUrl}
726
+ onChangeText={(text) => updateSettings({ gatewayUrl: text })}
727
+ placeholder="http://127.0.0.1:37374/mcp"
728
+ mono
729
+ />
730
+ </FormRow>
731
+ </Card>
732
+
733
+ <Card>
734
+ <Card.Header title="Visual Flair" subtitle="Corner radius, density, surface and accent" />
735
+ <FormRow label="Corner radius" description={`Active preset: "${settings.flairRadius}"`}>
736
+ <View style={{ flexDirection: "row", gap: 6, flexWrap: "wrap" }}>
737
+ {(["sharp", "rounded", "pill"] as const).map((r) => (
738
+ <Button
739
+ key={r}
740
+ label={r.toUpperCase()}
741
+ size="sm"
742
+ variant={settings.flairRadius === r ? "primary" : "ghost"}
743
+ onPress={() => {
744
+ triggerHaptic("light");
745
+ updateSettings({ flairRadius: r });
746
+ }}
747
+ />
748
+ ))}
749
+ </View>
750
+ </FormRow>
751
+ <FormRow label="Layout density" description={`Active density: "${settings.flairDensity}"`}>
752
+ <View style={{ flexDirection: "row", gap: 6, flexWrap: "wrap" }}>
753
+ {(["compact", "comfortable", "spacious"] as const).map((d) => (
754
+ <Button
755
+ key={d}
756
+ label={d.charAt(0).toUpperCase() + d.slice(1)}
757
+ size="sm"
758
+ variant={settings.flairDensity === d ? "primary" : "ghost"}
759
+ onPress={() => {
760
+ triggerHaptic("light");
761
+ updateSettings({ flairDensity: d });
762
+ }}
763
+ />
764
+ ))}
765
+ </View>
766
+ </FormRow>
767
+ <FormRow label="Surface treatment" description={`Active surface: "${settings.flairSurface}"`}>
768
+ <View style={{ flexDirection: "row", gap: 6, flexWrap: "wrap" }}>
769
+ {(["flat", "tinted", "elevated"] as const).map((s) => (
770
+ <Button
771
+ key={s}
772
+ label={s.charAt(0).toUpperCase() + s.slice(1)}
773
+ size="sm"
774
+ variant={settings.flairSurface === s ? "primary" : "ghost"}
775
+ onPress={() => {
776
+ triggerHaptic("light");
777
+ updateSettings({ flairSurface: s });
778
+ }}
779
+ />
780
+ ))}
781
+ </View>
782
+ </FormRow>
783
+ <FormRow label="Brand accent color" description={`Current accent: ${settings.flairAccentColor}`}>
784
+ <TextInput
785
+ value={settings.flairAccentColor}
786
+ onChangeText={(text) => updateSettings({ flairAccentColor: text })}
787
+ placeholder="#6366f1"
788
+ mono
789
+ />
790
+ </FormRow>
791
+ </Card>
792
+
793
+ <ActionBar align="space-between">
794
+ <Text style={{ fontSize: 11, color: colors.foregroundMuted }}>
795
+ {isUpdating ? "Saving to disk..." : "Saved to settings.json atomically"}
796
+ </Text>
797
+ <Button
798
+ label="Reset Defaults"
799
+ variant="secondary"
800
+ size="sm"
801
+ onPress={() => {
802
+ triggerHaptic("warning");
803
+ void resetSettings();
804
+ }}
805
+ />
806
+ </ActionBar>
807
+ </View>
808
+ );
809
+
810
+ return (
811
+ <PluginThemeProvider
812
+ theme={{ ...theme, colors: { ...theme.colors, accent: settings.flairAccentColor } }}
813
+ layout={layout}
814
+ flair={{
815
+ radius: settings.flairRadius,
816
+ density: settings.flairDensity,
817
+ surfaceStyle: settings.flairSurface,
818
+ accentColor: settings.flairAccentColor,
819
+ }}
820
+ >
821
+ <ModalBody refreshing={query.isFetching} onRefresh={() => void query.refetch()}>
822
+ <Tabs
823
+ tabs={TABS}
824
+ activeTab={activeTab}
825
+ onTabChange={(tab) => {
826
+ triggerHaptic("light");
827
+ setActiveTab(tab);
828
+ }}
829
+ mode="auto"
830
+ />
831
+ {activeTab === "servers" ? renderServers() : null}
832
+ {activeTab === "diagnostics" ? renderDiagnostics() : null}
833
+ {activeTab === "settings" ? renderSettings() : null}
834
+ {activeTab === "about" ? (
835
+ <AboutSection
836
+ name="MCP Tools"
837
+ description="Live MCP server inspector, health probes, diagnostics and tool runner for Paseo agents."
838
+ version={PLUGIN_VERSION}
839
+ author="xpufx"
840
+ repository="https://github.com/xpufx/paseo-mcp-tools"
841
+ issues="https://github.com/xpufx/paseo-mcp-tools/issues"
842
+ license="MIT"
843
+ density="tiny"
844
+ extraItems={[
845
+ { label: "Servers", value: `${query.data?.servers.length ?? 0}`, copyable: false },
846
+ { label: "Provider", value: query.data?.provider ?? "unknown", copyable: true },
847
+ { label: "Health Polling", value: settings.healthPollingRate, copyable: false },
848
+ ]}
849
+ />
850
+ ) : null}
851
+ <ActionBar align="flex-end">
852
+ <Button
853
+ label="Close"
854
+ variant="ghost"
855
+ onPress={() => {
856
+ triggerHaptic("light");
857
+ close();
858
+ }}
859
+ />
860
+ </ActionBar>
861
+ <View style={{ alignItems: "center", paddingVertical: 8 }}>
862
+ <Text style={{ fontSize: 10, color: colors.foregroundMuted, opacity: 0.7 }}>
863
+ {PLUGIN_ATTRIBUTION} {PLUGIN_VERSION}
864
+ </Text>
865
+ </View>
866
+ </ModalBody>
867
+ </PluginThemeProvider>
868
+ );
869
+ }
870
+
871
+ export function contributeClient(client: ComposerPillRegistrar) {
872
+ return registerComposerPill(client, {
873
+ id: "mcp-tools",
874
+ title: "MCP",
875
+ compactTitle: "MCP",
876
+ modalTitle: "MCP Tools",
877
+ icon: "Plug",
878
+ flair: {
879
+ radius: "rounded",
880
+ density: "comfortable",
881
+ accentColor: "#6366f1",
882
+ },
883
+ renderPill: (props) => <McpPillBody {...props} />,
884
+ renderModal: (props) => <McpModalContent {...props} />,
885
+ });
886
+ }