@miphamai/cli 0.2.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.
- package/README.md +76 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.jpg +0 -0
- package/bin/mipham +51 -0
- package/bin/mipham.ts +8 -0
- package/package.json +59 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
- package/skills/mipham/om-security.mipham-skill.md +21 -0
- package/skills/standard/code-review.SKILL.md +116 -0
- package/skills/standard/compassionate-communication.SKILL.md +80 -0
- package/skills/standard/doc-generator.SKILL.md +21 -0
- package/skills/standard/github-ops.SKILL.md +22 -0
- package/skills/standard/memory.SKILL.md +22 -0
- package/skills/standard/mipham-code-setup.SKILL.md +113 -0
- package/skills/standard/security-review.SKILL.md +122 -0
- package/skills/standard/self-review.SKILL.md +20 -0
- package/skills/standard/superpower.SKILL.md +19 -0
- package/skills/standard/tdd.SKILL.md +20 -0
- package/skills/standard/web-search.SKILL.md +23 -0
- package/src/agent/sub-agent.ts +122 -0
- package/src/config/defaults.ts +10 -0
- package/src/config/loader.ts +30 -0
- package/src/core/context.ts +135 -0
- package/src/core/engine.ts +193 -0
- package/src/core/hooks.ts +116 -0
- package/src/core/instructions.ts +126 -0
- package/src/core/permission.ts +54 -0
- package/src/core/session-store.ts +136 -0
- package/src/index.tsx +93 -0
- package/src/mcp/client.ts +170 -0
- package/src/mcp/protocol.ts +104 -0
- package/src/mcp/transport.ts +169 -0
- package/src/mcp/types.ts +112 -0
- package/src/providers/anthropic.ts +286 -0
- package/src/providers/bootstrap.ts +28 -0
- package/src/providers/openai-compat.ts +158 -0
- package/src/providers/registry.ts +70 -0
- package/src/security/path.ts +90 -0
- package/src/security/url.ts +96 -0
- package/src/shared/constants.ts +368 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +161 -0
- package/src/skills/loader.ts +109 -0
- package/src/skills/mipham/runtime.ts +63 -0
- package/src/skills/standard/runtime.ts +59 -0
- package/src/tools/agent/agent.ts +51 -0
- package/src/tools/agent/memory.ts +51 -0
- package/src/tools/agent/plan.ts +87 -0
- package/src/tools/agent/skill.ts +58 -0
- package/src/tools/exec/bash.ts +111 -0
- package/src/tools/exec/git.ts +63 -0
- package/src/tools/exec/task.ts +69 -0
- package/src/tools/file/edit.ts +57 -0
- package/src/tools/file/glob.ts +29 -0
- package/src/tools/file/grep.ts +52 -0
- package/src/tools/file/read.ts +38 -0
- package/src/tools/file/write.ts +27 -0
- package/src/tools/index.ts +126 -0
- package/src/tools/network/web-fetch.ts +61 -0
- package/src/tools/network/web-search.ts +32 -0
- package/src/tools/system/config.ts +68 -0
- package/src/tools/system/mcp.ts +75 -0
- package/src/ui/app.tsx +317 -0
- package/src/ui/chat.tsx +76 -0
- package/src/ui/commands.ts +2232 -0
- package/src/ui/input.tsx +83 -0
- package/src/ui/picker.tsx +209 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync, existsSync, statSync } from 'node:fs'
|
|
2
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
|
+
import { resolveSafe } from '../../security/path'
|
|
4
|
+
|
|
5
|
+
export const readTool: ToolDefinition = {
|
|
6
|
+
name: 'Read',
|
|
7
|
+
description: 'Read a file from the local filesystem. Supports offset and limit for large files.',
|
|
8
|
+
category: 'file',
|
|
9
|
+
permission: 'auto',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
file_path: { type: 'string', description: 'Absolute path to the file to read' },
|
|
14
|
+
offset: { type: 'integer', description: 'Line number to start reading from' },
|
|
15
|
+
limit: { type: 'integer', description: 'Number of lines to read' },
|
|
16
|
+
},
|
|
17
|
+
required: ['file_path'],
|
|
18
|
+
},
|
|
19
|
+
async execute(params, ctx) {
|
|
20
|
+
const filePath = resolveSafe(ctx.cwd, params.file_path as string)
|
|
21
|
+
if (!existsSync(filePath)) {
|
|
22
|
+
return { success: false, content: '', error: `File not found: ${filePath}` }
|
|
23
|
+
}
|
|
24
|
+
const stat = statSync(filePath)
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
return { success: false, content: '', error: `Path is a directory: ${filePath}` }
|
|
27
|
+
}
|
|
28
|
+
const offset = (params.offset as number) || 0
|
|
29
|
+
const limit = (params.limit as number) || 2000
|
|
30
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
31
|
+
const lines = content.split('\n')
|
|
32
|
+
const slice = lines.slice(offset, offset + limit)
|
|
33
|
+
const result = slice
|
|
34
|
+
.map((l, i) => `${String(offset + i + 1).padStart(6, ' ')}\t${l}`)
|
|
35
|
+
.join('\n')
|
|
36
|
+
return { success: true, content: result }
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
4
|
+
import { resolveSafe } from '../../security/path'
|
|
5
|
+
|
|
6
|
+
export const writeTool: ToolDefinition = {
|
|
7
|
+
name: 'Write',
|
|
8
|
+
description: 'Write a file to the local filesystem. Creates parent directories if needed.',
|
|
9
|
+
category: 'file',
|
|
10
|
+
permission: 'ask',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
file_path: { type: 'string', description: 'Absolute path to write to' },
|
|
15
|
+
content: { type: 'string', description: 'Content to write' },
|
|
16
|
+
},
|
|
17
|
+
required: ['file_path', 'content'],
|
|
18
|
+
},
|
|
19
|
+
async execute(params, ctx) {
|
|
20
|
+
const filePath = resolveSafe(ctx.cwd, params.file_path as string)
|
|
21
|
+
const content = params.content as string
|
|
22
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
23
|
+
writeFileSync(filePath, content, 'utf-8')
|
|
24
|
+
const lines = content.split('\n').length
|
|
25
|
+
return { success: true, content: `Wrote ${lines} lines to ${filePath}` }
|
|
26
|
+
},
|
|
27
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ToolDefinition, ToolResult } from '../shared/index.ts'
|
|
2
|
+
import { readTool } from './file/read'
|
|
3
|
+
import { writeTool } from './file/write'
|
|
4
|
+
import { editTool } from './file/edit'
|
|
5
|
+
import { globTool } from './file/glob'
|
|
6
|
+
import { grepTool } from './file/grep'
|
|
7
|
+
import { bashTool } from './exec/bash'
|
|
8
|
+
import { gitTool } from './exec/git'
|
|
9
|
+
import { taskTool } from './exec/task'
|
|
10
|
+
import { agentTool } from './agent/agent'
|
|
11
|
+
import { skillTool } from './agent/skill'
|
|
12
|
+
import { planTool } from './agent/plan'
|
|
13
|
+
import { memoryTool } from './agent/memory'
|
|
14
|
+
import { webFetchTool } from './network/web-fetch'
|
|
15
|
+
import { webSearchTool } from './network/web-search'
|
|
16
|
+
import { configTool } from './system/config'
|
|
17
|
+
import { mcpTool } from './system/mcp'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate tool parameters against the tool's JSON Schema definition.
|
|
21
|
+
* Returns an array of error messages (empty = valid).
|
|
22
|
+
*/
|
|
23
|
+
function validateParams(
|
|
24
|
+
schema: Record<string, unknown>,
|
|
25
|
+
params: Record<string, unknown>,
|
|
26
|
+
): string[] {
|
|
27
|
+
const errors: string[] = []
|
|
28
|
+
|
|
29
|
+
// Check required fields
|
|
30
|
+
const required = schema.required as string[] | undefined
|
|
31
|
+
if (required) {
|
|
32
|
+
for (const field of required) {
|
|
33
|
+
if (params[field] === undefined || params[field] === null) {
|
|
34
|
+
errors.push(`Missing required parameter: "${field}"`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check types for provided fields
|
|
40
|
+
const properties = schema.properties as
|
|
41
|
+
| Record<string, { type: string; enum?: string[] }>
|
|
42
|
+
| undefined
|
|
43
|
+
if (properties) {
|
|
44
|
+
for (const [key, def] of Object.entries(properties)) {
|
|
45
|
+
const value = params[key]
|
|
46
|
+
if (value === undefined || value === null) continue // already caught by required check
|
|
47
|
+
|
|
48
|
+
switch (def.type) {
|
|
49
|
+
case 'string':
|
|
50
|
+
if (typeof value !== 'string') errors.push(`"${key}" must be a string`)
|
|
51
|
+
else if (def.enum && !def.enum.includes(value)) {
|
|
52
|
+
errors.push(`"${key}" must be one of: ${def.enum.join(', ')}`)
|
|
53
|
+
}
|
|
54
|
+
break
|
|
55
|
+
case 'integer':
|
|
56
|
+
case 'number':
|
|
57
|
+
if (typeof value !== 'number') errors.push(`"${key}" must be a number`)
|
|
58
|
+
break
|
|
59
|
+
case 'boolean':
|
|
60
|
+
if (typeof value !== 'boolean') errors.push(`"${key}" must be a boolean`)
|
|
61
|
+
break
|
|
62
|
+
case 'object':
|
|
63
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
64
|
+
errors.push(`"${key}" must be an object`)
|
|
65
|
+
}
|
|
66
|
+
break
|
|
67
|
+
case 'array':
|
|
68
|
+
if (!Array.isArray(value)) errors.push(`"${key}" must be an array`)
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return errors
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wrap a tool's execute with parameter validation.
|
|
79
|
+
*/
|
|
80
|
+
function withValidation(tool: ToolDefinition): ToolDefinition {
|
|
81
|
+
const schema = tool.parameters as Record<string, unknown>
|
|
82
|
+
if (!schema || !schema.properties) return tool // no schema to validate against
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...tool,
|
|
86
|
+
async execute(params, ctx): Promise<ToolResult> {
|
|
87
|
+
const errors = validateParams(schema, params)
|
|
88
|
+
if (errors.length > 0) {
|
|
89
|
+
return { success: false, content: '', error: `Invalid parameters: ${errors.join('; ')}` }
|
|
90
|
+
}
|
|
91
|
+
return tool.execute(params, ctx)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createToolRegistry(): Map<string, ToolDefinition> {
|
|
97
|
+
const tools: ToolDefinition[] = [
|
|
98
|
+
// File tools
|
|
99
|
+
withValidation(readTool),
|
|
100
|
+
withValidation(writeTool),
|
|
101
|
+
withValidation(editTool),
|
|
102
|
+
withValidation(globTool),
|
|
103
|
+
withValidation(grepTool),
|
|
104
|
+
// Exec tools
|
|
105
|
+
withValidation(bashTool),
|
|
106
|
+
withValidation(gitTool),
|
|
107
|
+
withValidation(taskTool),
|
|
108
|
+
// Agent tools
|
|
109
|
+
withValidation(agentTool),
|
|
110
|
+
withValidation(skillTool),
|
|
111
|
+
withValidation(planTool),
|
|
112
|
+
withValidation(memoryTool),
|
|
113
|
+
// Network tools
|
|
114
|
+
withValidation(webFetchTool),
|
|
115
|
+
withValidation(webSearchTool),
|
|
116
|
+
// System tools
|
|
117
|
+
withValidation(configTool),
|
|
118
|
+
withValidation(mcpTool),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
const map = new Map<string, ToolDefinition>()
|
|
122
|
+
for (const tool of tools) {
|
|
123
|
+
map.set(tool.name, tool)
|
|
124
|
+
}
|
|
125
|
+
return map
|
|
126
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import { validateUrl } from '../../security/url'
|
|
3
|
+
|
|
4
|
+
export const webFetchTool: ToolDefinition = {
|
|
5
|
+
name: 'WebFetch',
|
|
6
|
+
description: 'Fetch content from a URL and process into markdown.',
|
|
7
|
+
category: 'network',
|
|
8
|
+
permission: 'auto',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
url: { type: 'string', format: 'uri', description: 'URL to fetch' },
|
|
13
|
+
prompt: { type: 'string', description: 'Prompt to run on fetched content' },
|
|
14
|
+
},
|
|
15
|
+
required: ['url'],
|
|
16
|
+
},
|
|
17
|
+
async execute(params, _ctx) {
|
|
18
|
+
const url = params.url as string
|
|
19
|
+
|
|
20
|
+
// SSRF protection: validate URL before fetching
|
|
21
|
+
const validationError = validateUrl(url)
|
|
22
|
+
if (validationError) {
|
|
23
|
+
return { success: false, content: '', error: validationError }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const controller = new AbortController()
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), 30_000) // 30s timeout
|
|
29
|
+
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
headers: { 'User-Agent': 'Mipham-Code/0.1.0' },
|
|
32
|
+
redirect: 'follow',
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
content: '',
|
|
42
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const html = await response.text()
|
|
46
|
+
// Simple HTML-to-text extraction
|
|
47
|
+
const text = html
|
|
48
|
+
.replace(/<[^>]*>/g, ' ')
|
|
49
|
+
.replace(/\s+/g, ' ')
|
|
50
|
+
.trim()
|
|
51
|
+
.slice(0, 50_000)
|
|
52
|
+
return { success: true, content: text }
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message =
|
|
55
|
+
err instanceof Error && err.name === 'AbortError'
|
|
56
|
+
? 'Request timed out (30s)'
|
|
57
|
+
: `Fetch failed: ${String(err)}`
|
|
58
|
+
return { success: false, content: '', error: message }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const webSearchTool: ToolDefinition = {
|
|
4
|
+
name: 'WebSearch',
|
|
5
|
+
description: 'Search the web. Returns result titles and URLs.',
|
|
6
|
+
category: 'network',
|
|
7
|
+
permission: 'auto',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
query: { type: 'string', minLength: 2, description: 'Search query' },
|
|
12
|
+
allowed_domains: {
|
|
13
|
+
type: 'array',
|
|
14
|
+
items: { type: 'string' },
|
|
15
|
+
description: 'Only include results from these domains',
|
|
16
|
+
},
|
|
17
|
+
blocked_domains: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: { type: 'string' },
|
|
20
|
+
description: 'Never include results from these domains',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['query'],
|
|
24
|
+
},
|
|
25
|
+
async execute(params, _ctx) {
|
|
26
|
+
const query = params.query as string
|
|
27
|
+
return {
|
|
28
|
+
success: true,
|
|
29
|
+
content: `WebSearch query: "${query}". Results would appear here when search API is configured.`,
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { parse as parseYaml, stringify } from 'yaml'
|
|
4
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
5
|
+
|
|
6
|
+
const MIPHAM_DIR = join(process.env.HOME || '~', '.mipham')
|
|
7
|
+
const USER_CONFIG = join(MIPHAM_DIR, 'config.yml')
|
|
8
|
+
|
|
9
|
+
export const configTool: ToolDefinition = {
|
|
10
|
+
name: 'Config',
|
|
11
|
+
description: 'Read or update Mipham Code configuration.',
|
|
12
|
+
category: 'system',
|
|
13
|
+
permission: 'ask',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
action: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['get', 'set', 'list'],
|
|
20
|
+
description: 'Action to perform',
|
|
21
|
+
},
|
|
22
|
+
key: { type: 'string', description: 'Config key (dot notation)' },
|
|
23
|
+
value: { type: 'string', description: 'Value to set' },
|
|
24
|
+
},
|
|
25
|
+
required: ['action'],
|
|
26
|
+
},
|
|
27
|
+
async execute(params, _ctx) {
|
|
28
|
+
mkdirSync(MIPHAM_DIR, { recursive: true })
|
|
29
|
+
const action = params.action as string
|
|
30
|
+
|
|
31
|
+
let config: Record<string, unknown> = {}
|
|
32
|
+
if (existsSync(USER_CONFIG)) {
|
|
33
|
+
config = parseYaml(readFileSync(USER_CONFIG, 'utf-8')) as Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (action === 'list') {
|
|
37
|
+
return { success: true, content: stringify(config) || '(empty config)' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const key = params.key as string
|
|
41
|
+
if (!key) return { success: false, content: '', error: 'key is required for get/set' }
|
|
42
|
+
|
|
43
|
+
if (action === 'get') {
|
|
44
|
+
const value = key.split('.').reduce((obj: unknown, k) => {
|
|
45
|
+
if (obj && typeof obj === 'object') {
|
|
46
|
+
return (obj as Record<string, unknown>)[k]
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}, config)
|
|
50
|
+
return { success: true, content: JSON.stringify(value) }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (action === 'set') {
|
|
54
|
+
const keys = key.split('.')
|
|
55
|
+
let obj: Record<string, unknown> = config
|
|
56
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
57
|
+
const k = keys[i]!
|
|
58
|
+
if (!obj[k]) obj[k] = {}
|
|
59
|
+
obj = obj[k] as Record<string, unknown>
|
|
60
|
+
}
|
|
61
|
+
obj[keys[keys.length - 1]!] = params.value
|
|
62
|
+
writeFileSync(USER_CONFIG, stringify(config), 'utf-8')
|
|
63
|
+
return { success: true, content: `Set ${key} = ${params.value}` }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { success: false, content: '', error: `Unknown action: ${action}` }
|
|
67
|
+
},
|
|
68
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import { McpClient } from '../../mcp/client'
|
|
3
|
+
|
|
4
|
+
export const mcpTool: ToolDefinition = {
|
|
5
|
+
name: 'MCP',
|
|
6
|
+
description:
|
|
7
|
+
'Interact with MCP (Model Context Protocol) servers. Call tools on connected MCP servers via JSON-RPC 2.0 over stdio.',
|
|
8
|
+
category: 'system',
|
|
9
|
+
permission: 'ask',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
server: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'MCP server name as configured in .mipham/config.yml',
|
|
16
|
+
},
|
|
17
|
+
tool: { type: 'string', description: 'Tool name on the MCP server' },
|
|
18
|
+
params: { type: 'object', description: 'Parameters to pass to the tool' },
|
|
19
|
+
},
|
|
20
|
+
required: ['server', 'tool'],
|
|
21
|
+
},
|
|
22
|
+
async execute(params, _ctx) {
|
|
23
|
+
const server = params.server as string
|
|
24
|
+
const toolName = params.tool as string
|
|
25
|
+
const toolParams = (params.params as Record<string, unknown>) || {}
|
|
26
|
+
|
|
27
|
+
const client = McpClient.getInstance()
|
|
28
|
+
const connection = client.getConnection(server)
|
|
29
|
+
|
|
30
|
+
if (!connection) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
content: '',
|
|
34
|
+
error: `MCP server "${server}" is not configured.\n\nAdd it to .mipham/config.yml under skills.mcpServers, or check available servers with /mcp.`,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (connection.status !== 'connected') {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
content: '',
|
|
42
|
+
error: `MCP server "${server}" is not connected (status: ${connection.status}).\n${connection.error ? 'Error: ' + connection.error : 'Use /mcp to check status.'}`,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if the requested tool exists on this server
|
|
47
|
+
const toolExists = connection.tools.some((t) => t.name === toolName)
|
|
48
|
+
if (!toolExists) {
|
|
49
|
+
const availableTools = connection.tools.map((t) => ` • ${t.name}`).join('\n')
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
content: '',
|
|
53
|
+
error: `Tool "${toolName}" not found on MCP server "${server}".\n\nAvailable tools:\n${availableTools || ' (none discovered)'}`,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Execute the tool via the MCP client
|
|
58
|
+
const result = await client.callTool(server, toolName, toolParams)
|
|
59
|
+
|
|
60
|
+
// Format the result content
|
|
61
|
+
const text = result.content
|
|
62
|
+
.map((c) => {
|
|
63
|
+
if (c.type === 'text' && c.text) return c.text
|
|
64
|
+
if (c.type === 'image') return `[Image: ${c.mimeType || 'unknown'}]`
|
|
65
|
+
return JSON.stringify(c)
|
|
66
|
+
})
|
|
67
|
+
.join('\n')
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
success: !result.isError,
|
|
71
|
+
content: text || '(empty result)',
|
|
72
|
+
error: result.isError ? text : undefined,
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|