feat(skill_mcp): add dynamic truncation and grep filtering
- Add skill_mcp and webfetch to TRUNCATABLE_TOOLS list - Add grep parameter for regex filtering of output lines - Prevents token overflow from large MCP responses 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -16,6 +16,9 @@ const TRUNCATABLE_TOOLS = [
|
|||||||
"ast_grep_search",
|
"ast_grep_search",
|
||||||
"interactive_bash",
|
"interactive_bash",
|
||||||
"Interactive_bash",
|
"Interactive_bash",
|
||||||
|
"skill_mcp",
|
||||||
|
"webfetch",
|
||||||
|
"WebFetch",
|
||||||
]
|
]
|
||||||
|
|
||||||
interface ToolOutputTruncatorOptions {
|
interface ToolOutputTruncatorOptions {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach, mock } from "bun:test"
|
import { describe, it, expect, beforeEach, mock } from "bun:test"
|
||||||
import { createSkillMcpTool } from "./tools"
|
import { createSkillMcpTool, applyGrepFilter } from "./tools"
|
||||||
import { SkillMcpManager } from "../../features/skill-mcp-manager"
|
import { SkillMcpManager } from "../../features/skill-mcp-manager"
|
||||||
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
|
||||||
|
|
||||||
@@ -147,5 +147,69 @@ describe("skill_mcp tool", () => {
|
|||||||
expect(tool.description.length).toBeLessThan(200)
|
expect(tool.description.length).toBeLessThan(200)
|
||||||
expect(tool.description).toContain("mcp_name")
|
expect(tool.description).toContain("mcp_name")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("includes grep parameter in schema", () => {
|
||||||
|
// #given / #when
|
||||||
|
const tool = createSkillMcpTool({
|
||||||
|
manager,
|
||||||
|
getLoadedSkills: () => [],
|
||||||
|
getSessionID: () => "session",
|
||||||
|
})
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(tool.description).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("applyGrepFilter", () => {
|
||||||
|
it("filters lines matching pattern", () => {
|
||||||
|
// #given
|
||||||
|
const output = `line1: hello world
|
||||||
|
line2: foo bar
|
||||||
|
line3: hello again
|
||||||
|
line4: baz qux`
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = applyGrepFilter(output, "hello")
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result).toContain("line1: hello world")
|
||||||
|
expect(result).toContain("line3: hello again")
|
||||||
|
expect(result).not.toContain("foo bar")
|
||||||
|
expect(result).not.toContain("baz qux")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns original output when pattern is undefined", () => {
|
||||||
|
// #given
|
||||||
|
const output = "some output"
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = applyGrepFilter(output, undefined)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result).toBe(output)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns message when no lines match", () => {
|
||||||
|
// #given
|
||||||
|
const output = "line1\nline2\nline3"
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = applyGrepFilter(output, "xyz")
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result).toContain("[grep] No lines matched pattern")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles invalid regex gracefully", () => {
|
||||||
|
// #given
|
||||||
|
const output = "some output"
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const result = applyGrepFilter(output, "[invalid")
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(result).toBe(output)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -87,6 +87,20 @@ function parseArguments(argsJson: string | undefined): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyGrepFilter(output: string, pattern: string | undefined): string {
|
||||||
|
if (!pattern) return output
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern, "i")
|
||||||
|
const lines = output.split("\n")
|
||||||
|
const filtered = lines.filter(line => regex.test(line))
|
||||||
|
return filtered.length > 0
|
||||||
|
? filtered.join("\n")
|
||||||
|
: `[grep] No lines matched pattern: ${pattern}`
|
||||||
|
} catch {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition {
|
export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition {
|
||||||
const { manager, getLoadedSkills, getSessionID } = options
|
const { manager, getLoadedSkills, getSessionID } = options
|
||||||
|
|
||||||
@@ -98,6 +112,7 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition
|
|||||||
resource_name: tool.schema.string().optional().describe("MCP resource URI to read"),
|
resource_name: tool.schema.string().optional().describe("MCP resource URI to read"),
|
||||||
prompt_name: tool.schema.string().optional().describe("MCP prompt to get"),
|
prompt_name: tool.schema.string().optional().describe("MCP prompt to get"),
|
||||||
arguments: tool.schema.string().optional().describe("JSON string of arguments"),
|
arguments: tool.schema.string().optional().describe("JSON string of arguments"),
|
||||||
|
grep: tool.schema.string().optional().describe("Regex pattern to filter output lines (only matching lines returned)"),
|
||||||
},
|
},
|
||||||
async execute(args: SkillMcpArgs) {
|
async execute(args: SkillMcpArgs) {
|
||||||
const operation = validateOperationParams(args)
|
const operation = validateOperationParams(args)
|
||||||
@@ -126,14 +141,17 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition
|
|||||||
|
|
||||||
const parsedArgs = parseArguments(args.arguments)
|
const parsedArgs = parseArguments(args.arguments)
|
||||||
|
|
||||||
|
let output: string
|
||||||
switch (operation.type) {
|
switch (operation.type) {
|
||||||
case "tool": {
|
case "tool": {
|
||||||
const result = await manager.callTool(info, context, operation.name, parsedArgs)
|
const result = await manager.callTool(info, context, operation.name, parsedArgs)
|
||||||
return JSON.stringify(result, null, 2)
|
output = JSON.stringify(result, null, 2)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case "resource": {
|
case "resource": {
|
||||||
const result = await manager.readResource(info, context, operation.name)
|
const result = await manager.readResource(info, context, operation.name)
|
||||||
return JSON.stringify(result, null, 2)
|
output = JSON.stringify(result, null, 2)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case "prompt": {
|
case "prompt": {
|
||||||
const stringArgs: Record<string, string> = {}
|
const stringArgs: Record<string, string> = {}
|
||||||
@@ -141,9 +159,11 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition
|
|||||||
stringArgs[key] = String(value)
|
stringArgs[key] = String(value)
|
||||||
}
|
}
|
||||||
const result = await manager.getPrompt(info, context, operation.name, stringArgs)
|
const result = await manager.getPrompt(info, context, operation.name, stringArgs)
|
||||||
return JSON.stringify(result, null, 2)
|
output = JSON.stringify(result, null, 2)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return applyGrepFilter(output, args.grep)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export interface SkillMcpArgs {
|
|||||||
resource_name?: string
|
resource_name?: string
|
||||||
prompt_name?: string
|
prompt_name?: string
|
||||||
arguments?: string
|
arguments?: string
|
||||||
|
grep?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user