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:
YeonGyu-Kim
2026-01-01 23:43:00 +09:00
parent a82575b55f
commit bd05f5b434
4 changed files with 92 additions and 4 deletions

View File

@@ -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 {

View File

@@ -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)
}) })
}) })

View File

@@ -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)
}, },
}) })
} }

View File

@@ -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
} }