@miphamai/cli 0.6.3 → 0.6.4
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/package.json +1 -1
- package/src/core/engine.ts +21 -0
- package/src/index.tsx +27 -12
- package/src/mcp/registry.ts +142 -0
package/package.json
CHANGED
package/src/core/engine.ts
CHANGED
|
@@ -431,6 +431,27 @@ export class QueryEngine {
|
|
|
431
431
|
return this.tools
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
/** Register a tool dynamically (used by MCP auto-registration). */
|
|
435
|
+
registerTool(tool: ToolDefinition): void {
|
|
436
|
+
if (this.tools.has(tool.name)) {
|
|
437
|
+
process.stderr.write(`[mcp] Tool collision: "${tool.name}" already registered. Skipping.\n`)
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
this.tools.set(tool.name, tool)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Remove a dynamically registered tool by name. */
|
|
444
|
+
unregisterTool(name: string): void {
|
|
445
|
+
this.tools.delete(name)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Register multiple tools at once. */
|
|
449
|
+
registerTools(tools: ToolDefinition[]): void {
|
|
450
|
+
for (const tool of tools) {
|
|
451
|
+
this.tools.set(tool.name, tool)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
434
455
|
switchProvider(providerId: string, modelId?: string): void {
|
|
435
456
|
this.registry.switchProvider(providerId, modelId)
|
|
436
457
|
}
|
package/src/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { SkillsLoader } from './skills/loader'
|
|
|
11
11
|
import { PluginManager } from './plugin/plugin-manager'
|
|
12
12
|
import { createToolRegistry } from './tools'
|
|
13
13
|
import { McpClient } from './mcp/client'
|
|
14
|
+
import { registerMcpServerTools } from './mcp/registry'
|
|
14
15
|
import { HookEngine } from './core/hooks'
|
|
15
16
|
import { ArtifactServer } from './artifacts/server'
|
|
16
17
|
import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
|
|
@@ -50,17 +51,6 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
50
51
|
// Load configuration
|
|
51
52
|
const config = loadConfig()
|
|
52
53
|
|
|
53
|
-
// Connect MCP servers (fire-and-forget — do not block startup)
|
|
54
|
-
const mcpServers = config.skills?.mcpServers ?? []
|
|
55
|
-
if (mcpServers.length > 0) {
|
|
56
|
-
const mcp = McpClient.getInstance()
|
|
57
|
-
for (const server of mcpServers) {
|
|
58
|
-
mcp.connect(server).catch((err) => {
|
|
59
|
-
process.stderr.write(`[mcp] Failed to connect "${server.name}": ${String(err)}\n`)
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
54
|
// Bootstrap providers
|
|
65
55
|
const defaultProvider = options.provider || config.defaultProvider
|
|
66
56
|
const defaultModel = options.model || config.defaultModel
|
|
@@ -97,9 +87,34 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
97
87
|
context.setSystemPrompt(instructions.buildSystemPrompt())
|
|
98
88
|
}
|
|
99
89
|
|
|
100
|
-
// Create tool registry with all
|
|
90
|
+
// Create tool registry with all built-in tools
|
|
101
91
|
const tools = createToolRegistry()
|
|
102
92
|
|
|
93
|
+
// Connect MCP servers and register their tools into the tool registry.
|
|
94
|
+
// Uses Promise.allSettled for parallel connection — failures are non-fatal.
|
|
95
|
+
const mcpServers = config.skills?.mcpServers ?? []
|
|
96
|
+
if (mcpServers.length > 0) {
|
|
97
|
+
const mcp = McpClient.getInstance()
|
|
98
|
+
const results = await Promise.allSettled(
|
|
99
|
+
mcpServers.map(async (server) => {
|
|
100
|
+
await mcp.connect(server)
|
|
101
|
+
const count = registerMcpServerTools(server.name, tools)
|
|
102
|
+
if (count > 0) {
|
|
103
|
+
process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
// Log failures (non-fatal — app starts without that server's tools)
|
|
108
|
+
for (let i = 0; i < results.length; i++) {
|
|
109
|
+
const result = results[i]!
|
|
110
|
+
if (result.status === 'rejected') {
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`[mcp] Failed to connect "${mcpServers[i]!.name}": ${String(result.reason)}\n`,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
// Initialize hook engine — register skill-defined hooks
|
|
104
119
|
const hookEngine = new HookEngine()
|
|
105
120
|
for (const skill of skillsLoader.list()) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Registry — converts MCP server tools into main ToolDefinitions
|
|
3
|
+
* and registers them into the central tool registry.
|
|
4
|
+
*
|
|
5
|
+
* Naming convention: mcp__<sanitizedServerName>__<sanitizedToolName>
|
|
6
|
+
* Example: server "chrome-devtools-mcp" tool "click" → "mcp__chrome_devtools_mcp__click"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ToolDefinition as McpToolDefinition, ToolCallResult } from './types'
|
|
10
|
+
import type { ToolDefinition, ToolResult, ToolContext } from '../shared/types'
|
|
11
|
+
import { McpClient } from './client'
|
|
12
|
+
|
|
13
|
+
const MCP_TOOL_PREFIX = 'mcp__'
|
|
14
|
+
const MAX_NAME_LENGTH = 128
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize a name for use in a tool identifier.
|
|
18
|
+
* Lowercases and replaces non-alphanumeric characters (except hyphens) with underscores.
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeName(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9-]/g, '_')
|
|
24
|
+
.replace(/_+/g, '_')
|
|
25
|
+
.replace(/^_|_$/g, '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a namespaced MCP tool name.
|
|
30
|
+
* Format: mcp__<sanitizedServerName>__<sanitizedToolName>
|
|
31
|
+
*/
|
|
32
|
+
function buildToolName(serverName: string, toolName: string): string {
|
|
33
|
+
const base = `${MCP_TOOL_PREFIX}${sanitizeName(serverName)}__${sanitizeName(toolName)}`
|
|
34
|
+
if (base.length <= MAX_NAME_LENGTH) return base
|
|
35
|
+
const prefix = `${MCP_TOOL_PREFIX}${sanitizeName(serverName)}__`
|
|
36
|
+
const maxToolLen = MAX_NAME_LENGTH - prefix.length
|
|
37
|
+
return prefix + sanitizeName(toolName).slice(0, maxToolLen)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert an MCP server ToolDefinition into the main ToolDefinition format
|
|
42
|
+
* that can be registered in the central tool registry.
|
|
43
|
+
*/
|
|
44
|
+
export function convertMcpTool(serverName: string, mcpTool: McpToolDefinition): ToolDefinition {
|
|
45
|
+
const toolName = buildToolName(serverName, mcpTool.name)
|
|
46
|
+
|
|
47
|
+
// Build parameters schema — pass through MCP inputSchema as-is
|
|
48
|
+
const parameters: Record<string, unknown> = {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: (mcpTool.inputSchema.properties as Record<string, unknown>) ?? {},
|
|
51
|
+
}
|
|
52
|
+
if (mcpTool.inputSchema.required) {
|
|
53
|
+
parameters.required = mcpTool.inputSchema.required
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: toolName,
|
|
58
|
+
description: `[MCP:${serverName}] ${mcpTool.description || mcpTool.name}`,
|
|
59
|
+
category: 'system',
|
|
60
|
+
permission: 'ask',
|
|
61
|
+
parameters,
|
|
62
|
+
async execute(params: Record<string, unknown>, _ctx: ToolContext): Promise<ToolResult> {
|
|
63
|
+
const client = McpClient.getInstance()
|
|
64
|
+
|
|
65
|
+
const conn = client.getConnection(serverName)
|
|
66
|
+
if (!conn || conn.status !== 'connected') {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
content: '',
|
|
70
|
+
error: `MCP server "${serverName}" is not connected (status: ${conn?.status || 'unknown'}). The server may have crashed or been disconnected.`,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result: ToolCallResult = await client.callTool(serverName, mcpTool.name, params)
|
|
75
|
+
|
|
76
|
+
const text = result.content
|
|
77
|
+
.map((c) => {
|
|
78
|
+
if (c.type === 'text' && c.text) return c.text
|
|
79
|
+
if (c.type === 'image') return `[Image: ${c.mimeType || 'unknown'}]`
|
|
80
|
+
return JSON.stringify(c)
|
|
81
|
+
})
|
|
82
|
+
.join('\n')
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
success: !result.isError,
|
|
86
|
+
content: text || '(empty result)',
|
|
87
|
+
error: result.isError ? text : undefined,
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register all tools from a connected MCP server into the tool registry.
|
|
95
|
+
* Returns the number of tools registered.
|
|
96
|
+
*/
|
|
97
|
+
export function registerMcpServerTools(
|
|
98
|
+
serverName: string,
|
|
99
|
+
toolsMap: Map<string, ToolDefinition>,
|
|
100
|
+
): number {
|
|
101
|
+
const client = McpClient.getInstance()
|
|
102
|
+
const mcpTools = client.getTools(serverName)
|
|
103
|
+
let registered = 0
|
|
104
|
+
|
|
105
|
+
for (const mcpTool of mcpTools) {
|
|
106
|
+
try {
|
|
107
|
+
const tool = convertMcpTool(serverName, mcpTool)
|
|
108
|
+
|
|
109
|
+
if (toolsMap.has(tool.name)) {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
`[mcp] Tool collision: "${tool.name}" (from server "${serverName}") conflicts with existing tool. Skipping.\n`,
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
toolsMap.set(tool.name, tool)
|
|
117
|
+
registered++
|
|
118
|
+
} catch (err) {
|
|
119
|
+
process.stderr.write(
|
|
120
|
+
`[mcp] Failed to register tool "${mcpTool.name}" from server "${serverName}": ${String(err)}\n`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return registered
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Unregister all tools previously registered from a specific MCP server.
|
|
130
|
+
* Tools are identified by the mcp__<serverName>__ prefix.
|
|
131
|
+
*/
|
|
132
|
+
export function unregisterMcpServerTools(
|
|
133
|
+
serverName: string,
|
|
134
|
+
toolsMap: Map<string, ToolDefinition>,
|
|
135
|
+
): void {
|
|
136
|
+
const prefix = `${MCP_TOOL_PREFIX}${sanitizeName(serverName)}__`
|
|
137
|
+
for (const name of toolsMap.keys()) {
|
|
138
|
+
if (name.startsWith(prefix)) {
|
|
139
|
+
toolsMap.delete(name)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|