@miphamai/cli 0.6.3 → 0.6.5
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 +29 -0
- package/src/index.tsx +35 -12
- package/src/mcp/registry.ts +142 -0
- package/src/shared/types.ts +2 -0
- package/src/skills/loader.ts +2 -1
- package/src/tools/agent/skill.ts +4 -3
package/package.json
CHANGED
package/src/core/engine.ts
CHANGED
|
@@ -8,12 +8,14 @@ import type { AgentRegistry } from '../agent/agent-registry'
|
|
|
8
8
|
import { analyzeForMemory } from './memory/memory-writer'
|
|
9
9
|
import { getMemoryManager } from './memory/memory-loader'
|
|
10
10
|
import type { AgentViewManager } from '../agent-view/agent-view-manager'
|
|
11
|
+
import type { SkillsLoader } from '../skills/loader'
|
|
11
12
|
|
|
12
13
|
export class QueryEngine {
|
|
13
14
|
private hookEngine?: HookEngine
|
|
14
15
|
private artifactServer?: ArtifactServer
|
|
15
16
|
private agentRegistry?: AgentRegistry
|
|
16
17
|
private agentViewManager?: AgentViewManager
|
|
18
|
+
private skillsLoader?: SkillsLoader
|
|
17
19
|
private goal?: string
|
|
18
20
|
private maxGoalLoops = 20
|
|
19
21
|
private lastAssistantContent?: string
|
|
@@ -55,6 +57,11 @@ export class QueryEngine {
|
|
|
55
57
|
return this.agentViewManager
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Register the SkillsLoader for Skill tool context injection. */
|
|
61
|
+
setSkillsLoader(loader: SkillsLoader): void {
|
|
62
|
+
this.skillsLoader = loader
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
/** Set the session goal for goal-driven execution. */
|
|
59
66
|
setGoal(goal: string): void {
|
|
60
67
|
this.goal = goal
|
|
@@ -393,6 +400,7 @@ export class QueryEngine {
|
|
|
393
400
|
sessionId: 'session-1',
|
|
394
401
|
provider: this.registry.getActive().config.id,
|
|
395
402
|
model: this.registry.getActiveModel(),
|
|
403
|
+
skillsLoader: this.skillsLoader,
|
|
396
404
|
registry: this.registry,
|
|
397
405
|
toolRegistry: this.tools,
|
|
398
406
|
artifactServer: this.artifactServer,
|
|
@@ -431,6 +439,27 @@ export class QueryEngine {
|
|
|
431
439
|
return this.tools
|
|
432
440
|
}
|
|
433
441
|
|
|
442
|
+
/** Register a tool dynamically (used by MCP auto-registration). */
|
|
443
|
+
registerTool(tool: ToolDefinition): void {
|
|
444
|
+
if (this.tools.has(tool.name)) {
|
|
445
|
+
process.stderr.write(`[mcp] Tool collision: "${tool.name}" already registered. Skipping.\n`)
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
this.tools.set(tool.name, tool)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Remove a dynamically registered tool by name. */
|
|
452
|
+
unregisterTool(name: string): void {
|
|
453
|
+
this.tools.delete(name)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Register multiple tools at once. */
|
|
457
|
+
registerTools(tools: ToolDefinition[]): void {
|
|
458
|
+
for (const tool of tools) {
|
|
459
|
+
this.tools.set(tool.name, tool)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
434
463
|
switchProvider(providerId: string, modelId?: string): void {
|
|
435
464
|
this.registry.switchProvider(providerId, modelId)
|
|
436
465
|
}
|
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
|
|
@@ -73,10 +63,17 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
73
63
|
// Load skills
|
|
74
64
|
const skillsLoader = new SkillsLoader()
|
|
75
65
|
skillsLoader.loadBuiltin(process.cwd())
|
|
66
|
+
skillsLoader.loadUserSkills()
|
|
76
67
|
if (config.skills?.paths) {
|
|
77
68
|
skillsLoader.loadExternal(config.skills.paths)
|
|
78
69
|
}
|
|
79
70
|
|
|
71
|
+
// Inject skills system-reminder into system prompt for AI auto-triggering
|
|
72
|
+
const skillsReminder = skillsLoader.buildSystemReminder()
|
|
73
|
+
if (skillsReminder) {
|
|
74
|
+
instructions.setSkillsReminder(skillsReminder)
|
|
75
|
+
}
|
|
76
|
+
|
|
80
77
|
// Initialize plugin manager
|
|
81
78
|
const pluginManager = new PluginManager()
|
|
82
79
|
|
|
@@ -97,9 +94,34 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
97
94
|
context.setSystemPrompt(instructions.buildSystemPrompt())
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
// Create tool registry with all
|
|
97
|
+
// Create tool registry with all built-in tools
|
|
101
98
|
const tools = createToolRegistry()
|
|
102
99
|
|
|
100
|
+
// Connect MCP servers and register their tools into the tool registry.
|
|
101
|
+
// Uses Promise.allSettled for parallel connection — failures are non-fatal.
|
|
102
|
+
const mcpServers = config.skills?.mcpServers ?? []
|
|
103
|
+
if (mcpServers.length > 0) {
|
|
104
|
+
const mcp = McpClient.getInstance()
|
|
105
|
+
const results = await Promise.allSettled(
|
|
106
|
+
mcpServers.map(async (server) => {
|
|
107
|
+
await mcp.connect(server)
|
|
108
|
+
const count = registerMcpServerTools(server.name, tools)
|
|
109
|
+
if (count > 0) {
|
|
110
|
+
process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
// Log failures (non-fatal — app starts without that server's tools)
|
|
115
|
+
for (let i = 0; i < results.length; i++) {
|
|
116
|
+
const result = results[i]!
|
|
117
|
+
if (result.status === 'rejected') {
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
`[mcp] Failed to connect "${mcpServers[i]!.name}": ${String(result.reason)}\n`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
// Initialize hook engine — register skill-defined hooks
|
|
104
126
|
const hookEngine = new HookEngine()
|
|
105
127
|
for (const skill of skillsLoader.list()) {
|
|
@@ -119,6 +141,7 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
119
141
|
engine.setHookEngine(hookEngine)
|
|
120
142
|
engine.setArtifactServer(artifactServer)
|
|
121
143
|
engine.setAgentViewManager(agentViewManager)
|
|
144
|
+
engine.setSkillsLoader(skillsLoader)
|
|
122
145
|
engine.setupContextSummarizer()
|
|
123
146
|
|
|
124
147
|
// Auto-save session on exit
|
|
@@ -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
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -147,6 +147,8 @@ export interface SkillDefinition {
|
|
|
147
147
|
disableModelInvocation?: boolean
|
|
148
148
|
/** When true, users can invoke this skill directly via /<name> */
|
|
149
149
|
userInvocable?: boolean
|
|
150
|
+
/** The markdown body content of the skill file (instructions for the AI to follow). */
|
|
151
|
+
body?: string
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
// ── Hook Types ──
|
package/src/skills/loader.ts
CHANGED
|
@@ -98,13 +98,14 @@ export class SkillsLoader {
|
|
|
98
98
|
private tryLoad(path: string, type: 'standard' | 'mipham'): void {
|
|
99
99
|
try {
|
|
100
100
|
const raw = readFileSync(path, 'utf-8')
|
|
101
|
-
const { data } = parseFrontmatter(raw)
|
|
101
|
+
const { data, content } = parseFrontmatter(raw)
|
|
102
102
|
|
|
103
103
|
const skill: SkillDefinition = {
|
|
104
104
|
name: (data.name as string) || this.nameFromPath(path),
|
|
105
105
|
description: (data.description as string) || '',
|
|
106
106
|
version: (data.version as string) || '0.1.0',
|
|
107
107
|
type,
|
|
108
|
+
body: content.trim() || undefined,
|
|
108
109
|
tools: data.tools as SkillDefinition['tools'],
|
|
109
110
|
hooks: data.hooks as SkillDefinition['hooks'],
|
|
110
111
|
prompts: data.prompts as SkillDefinition['prompts'],
|
package/src/tools/agent/skill.ts
CHANGED
|
@@ -64,15 +64,16 @@ export const skillTool: ToolDefinition = {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// Standard inline execution — return
|
|
67
|
+
// Standard inline execution — return skill body for AI to follow
|
|
68
68
|
const lines: string[] = [
|
|
69
69
|
`── Skill Invoked: ${skill.name} ──`,
|
|
70
70
|
`Type: ${skill.type} | Version: ${skill.version}`,
|
|
71
71
|
skill.description ? `Description: ${skill.description}` : '',
|
|
72
72
|
args ? `Arguments: ${args}` : '',
|
|
73
73
|
'',
|
|
74
|
-
'The AI should now follow
|
|
75
|
-
'
|
|
74
|
+
'The AI should now follow these instructions:',
|
|
75
|
+
'',
|
|
76
|
+
skill.body || '(no instructions body)',
|
|
76
77
|
].filter(Boolean)
|
|
77
78
|
|
|
78
79
|
return { success: true, content: lines.join('\n') }
|