@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -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 16 tools
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
+ }