mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
77
packages/builtin-tools/src/tools/MCPTool/MCPTool.ts
Normal file
77
packages/builtin-tools/src/tools/MCPTool/MCPTool.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { z } from 'zod/v4'
|
||||
import { buildTool, type ToolDef } from 'src/Tool.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import { isOutputLineTruncated } from 'src/utils/terminal.js'
|
||||
import { DESCRIPTION, PROMPT } from './prompt.js'
|
||||
import {
|
||||
renderToolResultMessage,
|
||||
renderToolUseMessage,
|
||||
renderToolUseProgressMessage,
|
||||
} from './UI.js'
|
||||
|
||||
// Allow any input object since MCP tools define their own schemas
|
||||
export const inputSchema = lazySchema(() => z.object({}).passthrough())
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
export const outputSchema = lazySchema(() =>
|
||||
z.string().describe('MCP tool execution result'),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
// Re-export MCPProgress from centralized types to break import cycles
|
||||
export type { MCPProgress } from 'src/types/tools.js'
|
||||
|
||||
export const MCPTool = buildTool({
|
||||
isMcp: true,
|
||||
// Overridden in mcpClient.ts with the real MCP tool name + args
|
||||
isOpenWorld() {
|
||||
return false
|
||||
},
|
||||
// Overridden in mcpClient.ts
|
||||
name: 'mcp',
|
||||
maxResultSizeChars: 100_000,
|
||||
// Overridden in mcpClient.ts
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
// Overridden in mcpClient.ts
|
||||
async prompt() {
|
||||
return PROMPT
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
// Overridden in mcpClient.ts
|
||||
async call() {
|
||||
return {
|
||||
data: '',
|
||||
}
|
||||
},
|
||||
async checkPermissions(): Promise<PermissionResult> {
|
||||
return {
|
||||
behavior: 'passthrough',
|
||||
message: 'MCPTool requires permission.',
|
||||
}
|
||||
},
|
||||
renderToolUseMessage,
|
||||
// Overridden in mcpClient.ts
|
||||
userFacingName: () => 'mcp',
|
||||
renderToolUseProgressMessage,
|
||||
renderToolResultMessage,
|
||||
isResultTruncated(output: Output): boolean {
|
||||
return isOutputLineTruncated(output)
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(content, toolUseID) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content,
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
395
packages/builtin-tools/src/tools/MCPTool/UI.tsx
Normal file
395
packages/builtin-tools/src/tools/MCPTool/UI.tsx
Normal file
@@ -0,0 +1,395 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import type { z } from 'zod/v4'
|
||||
import { ProgressBar } from '@anthropic/ink'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import {
|
||||
linkifyUrlsInText,
|
||||
OutputLine,
|
||||
} from 'src/components/shell/OutputLine.js'
|
||||
import { Ansi, Box, Text, stringWidth } from '@anthropic/ink'
|
||||
import { createHyperlink } from 'src/utils/hyperlink.js'
|
||||
import type { ToolProgressData } from 'src/Tool.js'
|
||||
import type { ProgressMessage } from 'src/types/message.js'
|
||||
import type { MCPProgress } from 'src/types/tools.js'
|
||||
import { formatNumber } from 'src/utils/format.js'
|
||||
|
||||
import {
|
||||
getContentSizeEstimate,
|
||||
type MCPToolResult,
|
||||
} from 'src/utils/mcpValidation.js'
|
||||
import { jsonParse, jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import type { inputSchema } from './MCPTool.js'
|
||||
|
||||
// Threshold for displaying warning about large MCP responses
|
||||
const MCP_OUTPUT_WARNING_THRESHOLD_TOKENS = 10_000
|
||||
|
||||
// In non-verbose mode, truncate individual input values to keep the header
|
||||
// compact. Matches BashTool's philosophy of showing enough to identify the
|
||||
// call without dumping the entire payload inline.
|
||||
const MAX_INPUT_VALUE_CHARS = 80
|
||||
|
||||
// Max number of top-level keys before we fall back to raw JSON display.
|
||||
// Beyond this a flat k:v list is more noise than help.
|
||||
const MAX_FLAT_JSON_KEYS = 12
|
||||
|
||||
// Don't attempt flat-object parsing for large blobs.
|
||||
const MAX_FLAT_JSON_CHARS = 5_000
|
||||
|
||||
// Don't attempt to parse JSON blobs larger than this (perf safety).
|
||||
const MAX_JSON_PARSE_CHARS = 200_000
|
||||
|
||||
// A string value is "dominant text payload" if it has newlines or is
|
||||
// long enough that inline display would be worse than unwrapping.
|
||||
const UNWRAP_MIN_STRING_LEN = 200
|
||||
|
||||
export function renderToolUseMessage(
|
||||
input: z.infer<ReturnType<typeof inputSchema>>,
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (Object.keys(input).length === 0) {
|
||||
return ''
|
||||
}
|
||||
return Object.entries(input)
|
||||
.map(([key, value]) => {
|
||||
let rendered = jsonStringify(value)
|
||||
if (
|
||||
feature('MCP_RICH_OUTPUT') &&
|
||||
!verbose &&
|
||||
rendered.length > MAX_INPUT_VALUE_CHARS
|
||||
) {
|
||||
rendered = rendered.slice(0, MAX_INPUT_VALUE_CHARS).trimEnd() + '…'
|
||||
}
|
||||
return `${key}: ${rendered}`
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
export function renderToolUseProgressMessage(
|
||||
progressMessagesForMessage: ProgressMessage<MCPProgress>[],
|
||||
): React.ReactNode {
|
||||
const lastProgress = progressMessagesForMessage.at(-1)
|
||||
|
||||
if (!lastProgress?.data) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Running…</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
const { progress, total, progressMessage } = lastProgress.data
|
||||
|
||||
if (progress === undefined) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Running…</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
if (total !== undefined && total > 0) {
|
||||
const ratio = Math.min(1, Math.max(0, progress / total))
|
||||
const percentage = Math.round(ratio * 100)
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
{progressMessage && <Text dimColor>{progressMessage}</Text>}
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<ProgressBar ratio={ratio} width={20} />
|
||||
<Text dimColor>{percentage}%</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>{progressMessage ?? `Processing… ${progress}`}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderToolResultMessage(
|
||||
output: string | MCPToolResult,
|
||||
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
||||
{ verbose, input }: { verbose: boolean; input?: unknown },
|
||||
): React.ReactNode {
|
||||
const mcpOutput = output as MCPToolResult
|
||||
|
||||
if (!verbose) {
|
||||
const slackSend = trySlackSendCompact(mcpOutput, input)
|
||||
if (slackSend !== null) {
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
<Text>
|
||||
Sent a message to{' '}
|
||||
<Ansi>{createHyperlink(slackSend.url, slackSend.channel)}</Ansi>
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const estimatedTokens = getContentSizeEstimate(mcpOutput)
|
||||
const showWarning = estimatedTokens > MCP_OUTPUT_WARNING_THRESHOLD_TOKENS
|
||||
const warningMessage = showWarning
|
||||
? `${figures.warning} Large MCP response (~${formatNumber(estimatedTokens)} tokens), this can fill up context quickly`
|
||||
: null
|
||||
|
||||
let contentElement: React.ReactNode
|
||||
if (Array.isArray(mcpOutput)) {
|
||||
const contentBlocks = mcpOutput.map((item, i) => {
|
||||
if (item.type === 'image') {
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
justifyContent="space-between"
|
||||
overflowX="hidden"
|
||||
width="100%"
|
||||
>
|
||||
<MessageResponse height={1}>
|
||||
<Text>[Image]</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
// For text blocks and any other block types, extract text if available
|
||||
const textContent =
|
||||
item.type === 'text' &&
|
||||
'text' in item &&
|
||||
item.text !== null &&
|
||||
item.text !== undefined
|
||||
? String(item.text)
|
||||
: ''
|
||||
return feature('MCP_RICH_OUTPUT') ? (
|
||||
<MCPTextOutput key={i} content={textContent} verbose={verbose} />
|
||||
) : (
|
||||
<OutputLine key={i} content={textContent} verbose={verbose} />
|
||||
)
|
||||
})
|
||||
|
||||
// Wrap array content in a column layout
|
||||
contentElement = (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{contentBlocks}
|
||||
</Box>
|
||||
)
|
||||
} else if (!mcpOutput) {
|
||||
contentElement = (
|
||||
<Box justifyContent="space-between" overflowX="hidden" width="100%">
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>(No content)</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
contentElement = feature('MCP_RICH_OUTPUT') ? (
|
||||
<MCPTextOutput content={mcpOutput} verbose={verbose} />
|
||||
) : (
|
||||
<OutputLine content={mcpOutput} verbose={verbose} />
|
||||
)
|
||||
}
|
||||
|
||||
if (warningMessage) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<MessageResponse height={1}>
|
||||
<Text color="warning">{warningMessage}</Text>
|
||||
</MessageResponse>
|
||||
{contentElement}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return contentElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Render MCP text output. Tries three strategies in order:
|
||||
* 1. If JSON wraps a single dominant text payload (e.g. slack's
|
||||
* {"messages":"line1\nline2..."}), unwrap and let OutputLine truncate.
|
||||
* 2. If JSON is a small flat-ish object, render as aligned key: value.
|
||||
* 3. Otherwise fall through to OutputLine (pretty-print + truncate).
|
||||
*/
|
||||
function MCPTextOutput({
|
||||
content,
|
||||
verbose,
|
||||
}: {
|
||||
content: string
|
||||
verbose: boolean
|
||||
}): React.ReactNode {
|
||||
const unwrapped = tryUnwrapTextPayload(content)
|
||||
if (unwrapped !== null) {
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
{unwrapped.extras.length > 0 && (
|
||||
<Text dimColor>
|
||||
{unwrapped.extras.map(([k, v]) => `${k}: ${v}`).join(' · ')}
|
||||
</Text>
|
||||
)}
|
||||
<OutputLine content={unwrapped.body} verbose={verbose} linkifyUrls />
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
const flat = tryFlattenJson(content)
|
||||
if (flat !== null) {
|
||||
const maxKeyWidth = Math.max(...flat.map(([k]) => stringWidth(k)))
|
||||
return (
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
{flat.map(([key, value], i) => (
|
||||
<Text key={i}>
|
||||
<Text dimColor>{key.padEnd(maxKeyWidth)}: </Text>
|
||||
<Ansi>{linkifyUrlsInText(value)}</Ansi>
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
)
|
||||
}
|
||||
return <OutputLine content={content} verbose={verbose} linkifyUrls />
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse content as a JSON object and return its entries. Null if content
|
||||
* doesn't parse, isn't an object, is too large, or has 0/too-many keys.
|
||||
*/
|
||||
function parseJsonEntries(
|
||||
content: string,
|
||||
{ maxChars, maxKeys }: { maxChars: number; maxKeys: number },
|
||||
): [string, unknown][] | null {
|
||||
const trimmed = content.trim()
|
||||
if (trimmed.length === 0 || trimmed.length > maxChars || trimmed[0] !== '{') {
|
||||
return null
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = jsonParse(trimmed)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return null
|
||||
}
|
||||
const entries = Object.entries(parsed)
|
||||
if (entries.length === 0 || entries.length > maxKeys) {
|
||||
return null
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
/**
|
||||
* If content parses as a JSON object where every value is a scalar or a
|
||||
* small nested object, flatten it to [key, displayValue] pairs. Nested
|
||||
* objects get one-line JSON. Returns null if content doesn't qualify.
|
||||
*/
|
||||
export function tryFlattenJson(content: string): [string, string][] | null {
|
||||
const entries = parseJsonEntries(content, {
|
||||
maxChars: MAX_FLAT_JSON_CHARS,
|
||||
maxKeys: MAX_FLAT_JSON_KEYS,
|
||||
})
|
||||
if (entries === null) return null
|
||||
const result: [string, string][] = []
|
||||
for (const [key, value] of entries) {
|
||||
if (typeof value === 'string') {
|
||||
result.push([key, value])
|
||||
} else if (
|
||||
value === null ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean'
|
||||
) {
|
||||
result.push([key, String(value)])
|
||||
} else if (typeof value === 'object') {
|
||||
const compact = jsonStringify(value)
|
||||
if (compact.length > 120) return null
|
||||
result.push([key, compact])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* If content is a JSON object where one key holds a dominant string payload
|
||||
* (multiline or long) and all siblings are small scalars, unwrap it. This
|
||||
* handles the common MCP pattern of {"messages":"line1\nline2..."} where
|
||||
* pretty-printing keeps \n escaped but we want real line breaks + truncation.
|
||||
*/
|
||||
export function tryUnwrapTextPayload(
|
||||
content: string,
|
||||
): { body: string; extras: [string, string][] } | null {
|
||||
const entries = parseJsonEntries(content, {
|
||||
maxChars: MAX_JSON_PARSE_CHARS,
|
||||
maxKeys: 4,
|
||||
})
|
||||
if (entries === null) return null
|
||||
// Find the one dominant string payload. Trim first: a trailing \n on a
|
||||
// short sibling (e.g. pagination hints) shouldn't make it "dominant".
|
||||
let body: string | null = null
|
||||
const extras: [string, string][] = []
|
||||
for (const [key, value] of entries) {
|
||||
if (typeof value === 'string') {
|
||||
const t = value.trimEnd()
|
||||
const isDominant =
|
||||
t.length > UNWRAP_MIN_STRING_LEN || (t.includes('\n') && t.length > 50)
|
||||
if (isDominant) {
|
||||
if (body !== null) return null // two big strings — ambiguous
|
||||
body = t
|
||||
continue
|
||||
}
|
||||
if (t.length > 150) return null
|
||||
extras.push([key, t.replace(/\s+/g, ' ')])
|
||||
} else if (
|
||||
value === null ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean'
|
||||
) {
|
||||
extras.push([key, String(value)])
|
||||
} else {
|
||||
return null // nested object/array — use flat or pretty-print path
|
||||
}
|
||||
}
|
||||
if (body === null) return null
|
||||
return { body, extras }
|
||||
}
|
||||
|
||||
const SLACK_ARCHIVES_RE =
|
||||
/^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/([A-Z0-9]+)\/p\d+$/
|
||||
|
||||
/**
|
||||
* Detect a Slack send-message result and return a compact {channel, url} pair.
|
||||
* Matches both hosted (claude.ai Slack) and community MCP server shapes —
|
||||
* both return `message_link` in the result. The channel label prefers the
|
||||
* tool input (may be a name like "#foo" or an ID like "C09EVDAN1NK") and
|
||||
* falls back to the ID parsed from the archives URL.
|
||||
*/
|
||||
export function trySlackSendCompact(
|
||||
output: string | MCPToolResult,
|
||||
input: unknown,
|
||||
): { channel: string; url: string } | null {
|
||||
let text: unknown = output
|
||||
if (Array.isArray(output)) {
|
||||
const block = output.find(b => b.type === 'text')
|
||||
text = block && 'text' in block ? block.text : undefined
|
||||
}
|
||||
if (typeof text !== 'string' || !text.includes('"message_link"')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const entries = parseJsonEntries(text, { maxChars: 2000, maxKeys: 6 })
|
||||
const url = entries?.find(([k]) => k === 'message_link')?.[1]
|
||||
if (typeof url !== 'string') return null
|
||||
const m = SLACK_ARCHIVES_RE.exec(url)
|
||||
if (!m) return null
|
||||
|
||||
const inp = input as { channel_id?: unknown; channel?: unknown } | undefined
|
||||
const raw = inp?.channel_id ?? inp?.channel ?? m[1]
|
||||
const label = typeof raw === 'string' && raw ? raw : 'slack'
|
||||
return { channel: label.startsWith('#') ? label : `#${label}`, url }
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { classifyMcpToolForCollapse } from "../classifyForCollapse";
|
||||
|
||||
describe("classifyMcpToolForCollapse", () => {
|
||||
// Search tools
|
||||
test("classifies Slack slack_search_public as search", () => {
|
||||
expect(classifyMcpToolForCollapse("slack", "slack_search_public")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies GitHub search_code as search", () => {
|
||||
expect(classifyMcpToolForCollapse("github", "search_code")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Linear search_issues as search", () => {
|
||||
expect(classifyMcpToolForCollapse("linear", "search_issues")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Datadog search_logs as search", () => {
|
||||
expect(classifyMcpToolForCollapse("datadog", "search_logs")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Notion search as search", () => {
|
||||
expect(classifyMcpToolForCollapse("notion", "search")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Brave brave_web_search as search", () => {
|
||||
expect(classifyMcpToolForCollapse("brave-search", "brave_web_search")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Read tools
|
||||
test("classifies Slack slack_read_channel as read", () => {
|
||||
expect(classifyMcpToolForCollapse("slack", "slack_read_channel")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies GitHub get_file_contents as read", () => {
|
||||
expect(classifyMcpToolForCollapse("github", "get_file_contents")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Linear get_issue as read", () => {
|
||||
expect(classifyMcpToolForCollapse("linear", "get_issue")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Filesystem read_file as read", () => {
|
||||
expect(classifyMcpToolForCollapse("filesystem", "read_file")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies GitHub list_commits as read", () => {
|
||||
expect(classifyMcpToolForCollapse("github", "list_commits")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("classifies Slack slack_list_channels as read", () => {
|
||||
expect(classifyMcpToolForCollapse("slack", "slack_list_channels")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Unknown tools
|
||||
test("unknown tool returns { isSearch: false, isRead: false }", () => {
|
||||
expect(classifyMcpToolForCollapse("unknown", "do_something")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
// normalize: camelCase -> snake_case
|
||||
test("tool name with camelCase variant still matches after normalize", () => {
|
||||
// searchCode -> search_code
|
||||
expect(classifyMcpToolForCollapse("github", "searchCode")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
// normalize: kebab-case -> snake_case
|
||||
test("tool name with kebab-case variant still matches after normalize", () => {
|
||||
// search-code -> search_code
|
||||
expect(classifyMcpToolForCollapse("github", "search-code")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Server name doesn't affect classification
|
||||
test("server name parameter does not affect classification", () => {
|
||||
const r1 = classifyMcpToolForCollapse("server-a", "search_code");
|
||||
const r2 = classifyMcpToolForCollapse("server-b", "search_code");
|
||||
expect(r1).toEqual(r2);
|
||||
});
|
||||
|
||||
// Edge cases
|
||||
test("empty tool name returns false/false", () => {
|
||||
expect(classifyMcpToolForCollapse("server", "")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
// normalize lowercases, so SEARCH_CODE -> search_code -> matches
|
||||
test("uppercase input normalizes to match", () => {
|
||||
expect(classifyMcpToolForCollapse("github", "SEARCH_CODE")).toEqual({
|
||||
isSearch: true,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("handles tool names with numbers", () => {
|
||||
expect(classifyMcpToolForCollapse("server", "search2_things")).toEqual({
|
||||
isSearch: false,
|
||||
isRead: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
604
packages/builtin-tools/src/tools/MCPTool/classifyForCollapse.ts
Normal file
604
packages/builtin-tools/src/tools/MCPTool/classifyForCollapse.ts
Normal file
@@ -0,0 +1,604 @@
|
||||
/**
|
||||
* Classify an MCP tool as a search/read operation for UI collapsing.
|
||||
* Returns { isSearch: false, isRead: false } for tools that should not
|
||||
* collapse (e.g., send_message, create_*, update_*).
|
||||
*
|
||||
* Uses explicit per-tool allowlists for the most common MCP servers.
|
||||
* Tool names are stable across installs (even when the server name varies,
|
||||
* e.g., "slack" vs "claude_ai_Slack"), so matching is keyed on the tool
|
||||
* name alone after normalizing camelCase/kebab-case to snake_case.
|
||||
* Unknown tool names don't collapse (conservative).
|
||||
*/
|
||||
|
||||
// prettier-ignore
|
||||
const SEARCH_TOOLS = new Set([
|
||||
// Slack (hosted + @modelcontextprotocol/server-slack)
|
||||
'slack_search_public',
|
||||
'slack_search_public_and_private',
|
||||
'slack_search_channels',
|
||||
'slack_search_users',
|
||||
// GitHub (github/github-mcp-server)
|
||||
'search_code',
|
||||
'search_repositories',
|
||||
'search_issues',
|
||||
'search_pull_requests',
|
||||
'search_orgs',
|
||||
'search_users',
|
||||
// Linear (mcp.linear.app)
|
||||
'search_documentation',
|
||||
// Datadog (mcp.datadoghq.com)
|
||||
'search_logs',
|
||||
'search_spans',
|
||||
'search_rum_events',
|
||||
'search_audit_logs',
|
||||
'search_monitors',
|
||||
'search_monitor_groups',
|
||||
'find_slow_spans',
|
||||
'find_monitors_matching_pattern',
|
||||
// Sentry (getsentry/sentry-mcp)
|
||||
'search_docs',
|
||||
'search_events',
|
||||
'search_issue_events',
|
||||
'find_organizations',
|
||||
'find_teams',
|
||||
'find_projects',
|
||||
'find_releases',
|
||||
'find_dsns',
|
||||
// Notion (mcp.notion.com — kebab-case, normalized)
|
||||
'search',
|
||||
// Gmail (claude.ai hosted)
|
||||
'gmail_search_messages',
|
||||
// Google Drive (claude.ai hosted + @modelcontextprotocol/server-gdrive)
|
||||
'google_drive_search',
|
||||
// Google Calendar (claude.ai hosted)
|
||||
'gcal_find_my_free_time',
|
||||
'gcal_find_meeting_times',
|
||||
'gcal_find_user_emails',
|
||||
// Atlassian/Jira (mcp.atlassian.com — camelCase, normalized)
|
||||
'search_jira_issues_using_jql',
|
||||
'search_confluence_using_cql',
|
||||
'lookup_jira_account_id',
|
||||
// Community Atlassian (sooperset/mcp-atlassian)
|
||||
'confluence_search',
|
||||
'jira_search',
|
||||
'jira_search_fields',
|
||||
// Asana (mcp.asana.com)
|
||||
'asana_search_tasks',
|
||||
'asana_typeahead_search',
|
||||
// Filesystem (@modelcontextprotocol/server-filesystem)
|
||||
'search_files',
|
||||
// Memory (@modelcontextprotocol/server-memory)
|
||||
'search_nodes',
|
||||
// Brave Search
|
||||
'brave_web_search',
|
||||
'brave_local_search',
|
||||
// Git (mcp-server-git)
|
||||
// (git has no search verbs)
|
||||
// Grafana (grafana/mcp-grafana)
|
||||
'search_dashboards',
|
||||
'search_folders',
|
||||
// PagerDuty
|
||||
// (pagerduty reads all use get_/list_, no search verbs)
|
||||
// Supabase
|
||||
'search_docs',
|
||||
// Stripe
|
||||
'search_stripe_resources',
|
||||
'search_stripe_documentation',
|
||||
// PubMed (claude.ai hosted + community)
|
||||
'search_articles',
|
||||
'find_related_articles',
|
||||
'lookup_article_by_citation',
|
||||
'search_papers',
|
||||
'search_pubmed',
|
||||
'search_pubmed_key_words',
|
||||
'search_pubmed_advanced',
|
||||
'pubmed_search',
|
||||
'pubmed_mesh_lookup',
|
||||
// Firecrawl
|
||||
'firecrawl_search',
|
||||
// Exa
|
||||
'web_search_exa',
|
||||
'web_search_advanced_exa',
|
||||
'people_search_exa',
|
||||
'linkedin_search_exa',
|
||||
'deep_search_exa',
|
||||
// Perplexity
|
||||
'perplexity_search',
|
||||
'perplexity_search_web',
|
||||
// Tavily
|
||||
'tavily_search',
|
||||
// Obsidian (MarkusPfundstein)
|
||||
'obsidian_simple_search',
|
||||
'obsidian_complex_search',
|
||||
// MongoDB
|
||||
'find',
|
||||
'search_knowledge',
|
||||
// Neo4j
|
||||
'search_memories',
|
||||
'find_memories_by_name',
|
||||
// Airtable
|
||||
'search_records',
|
||||
// Todoist (Doist — kebab-case, normalized)
|
||||
'find_tasks',
|
||||
'find_tasks_by_date',
|
||||
'find_completed_tasks',
|
||||
'find_projects',
|
||||
'find_sections',
|
||||
'find_comments',
|
||||
'find_project_collaborators',
|
||||
'find_activity',
|
||||
'find_labels',
|
||||
'find_filters',
|
||||
// AWS
|
||||
'search_documentation',
|
||||
'search_catalog',
|
||||
// Terraform
|
||||
'search_modules',
|
||||
'search_providers',
|
||||
'search_policies',
|
||||
])
|
||||
|
||||
// prettier-ignore
|
||||
const READ_TOOLS = new Set([
|
||||
// Slack (hosted + @modelcontextprotocol/server-slack)
|
||||
'slack_read_channel',
|
||||
'slack_read_thread',
|
||||
'slack_read_canvas',
|
||||
'slack_read_user_profile',
|
||||
'slack_list_channels',
|
||||
'slack_get_channel_history',
|
||||
'slack_get_thread_replies',
|
||||
'slack_get_users',
|
||||
'slack_get_user_profile',
|
||||
// GitHub (github/github-mcp-server)
|
||||
'get_me',
|
||||
'get_team_members',
|
||||
'get_teams',
|
||||
'get_commit',
|
||||
'get_file_contents',
|
||||
'get_repository_tree',
|
||||
'list_branches',
|
||||
'list_commits',
|
||||
'list_releases',
|
||||
'list_tags',
|
||||
'get_latest_release',
|
||||
'get_release_by_tag',
|
||||
'get_tag',
|
||||
'list_issues',
|
||||
'issue_read',
|
||||
'list_issue_types',
|
||||
'get_label',
|
||||
'list_label',
|
||||
'pull_request_read',
|
||||
'get_gist',
|
||||
'list_gists',
|
||||
'list_notifications',
|
||||
'get_notification_details',
|
||||
'projects_list',
|
||||
'projects_get',
|
||||
'actions_get',
|
||||
'actions_list',
|
||||
'get_job_logs',
|
||||
'get_code_scanning_alert',
|
||||
'list_code_scanning_alerts',
|
||||
'get_dependabot_alert',
|
||||
'list_dependabot_alerts',
|
||||
'get_secret_scanning_alert',
|
||||
'list_secret_scanning_alerts',
|
||||
'get_global_security_advisory',
|
||||
'list_global_security_advisories',
|
||||
'list_org_repository_security_advisories',
|
||||
'list_repository_security_advisories',
|
||||
'get_discussion',
|
||||
'get_discussion_comments',
|
||||
'list_discussion_categories',
|
||||
'list_discussions',
|
||||
'list_starred_repositories',
|
||||
'get_issue',
|
||||
'get_pull_request',
|
||||
'list_pull_requests',
|
||||
'get_pull_request_files',
|
||||
'get_pull_request_status',
|
||||
'get_pull_request_comments',
|
||||
'get_pull_request_reviews',
|
||||
// Linear (mcp.linear.app)
|
||||
'list_comments',
|
||||
'list_cycles',
|
||||
'get_document',
|
||||
'list_documents',
|
||||
'list_issue_statuses',
|
||||
'get_issue_status',
|
||||
'list_my_issues',
|
||||
'list_issue_labels',
|
||||
'list_projects',
|
||||
'get_project',
|
||||
'list_project_labels',
|
||||
'list_teams',
|
||||
'get_team',
|
||||
'list_users',
|
||||
'get_user',
|
||||
// Datadog (mcp.datadoghq.com)
|
||||
'aggregate_logs',
|
||||
'list_spans',
|
||||
'aggregate_spans',
|
||||
'analyze_trace',
|
||||
'trace_critical_path',
|
||||
'query_metrics',
|
||||
'aggregate_rum_events',
|
||||
'list_rum_metrics',
|
||||
'get_rum_metric',
|
||||
'list_monitors',
|
||||
'get_monitor',
|
||||
'check_can_delete_monitor',
|
||||
'validate_monitor',
|
||||
'validate_existing_monitor',
|
||||
'list_dashboards',
|
||||
'get_dashboard',
|
||||
'query_dashboard_widget',
|
||||
'list_notebooks',
|
||||
'get_notebook',
|
||||
'query_notebook_cell',
|
||||
'get_profiling_metrics',
|
||||
'compare_profiling_metrics',
|
||||
// Sentry (getsentry/sentry-mcp)
|
||||
'whoami',
|
||||
'get_issue_details',
|
||||
'get_issue_tag_values',
|
||||
'get_trace_details',
|
||||
'get_event_attachment',
|
||||
'get_doc',
|
||||
'get_sentry_resource',
|
||||
'list_events',
|
||||
'list_issue_events',
|
||||
'get_sentry_issue',
|
||||
// Notion (mcp.notion.com — kebab-case, normalized)
|
||||
'fetch',
|
||||
'get_comments',
|
||||
'get_users',
|
||||
'get_self',
|
||||
// Gmail (claude.ai hosted)
|
||||
'gmail_get_profile',
|
||||
'gmail_read_message',
|
||||
'gmail_read_thread',
|
||||
'gmail_list_drafts',
|
||||
'gmail_list_labels',
|
||||
// Google Drive (claude.ai hosted + @modelcontextprotocol/server-gdrive)
|
||||
'google_drive_fetch',
|
||||
'google_drive_export',
|
||||
// Google Calendar (claude.ai hosted)
|
||||
'gcal_list_calendars',
|
||||
'gcal_list_events',
|
||||
'gcal_get_event',
|
||||
// Atlassian/Jira (mcp.atlassian.com — camelCase, normalized)
|
||||
'atlassian_user_info',
|
||||
'get_accessible_atlassian_resources',
|
||||
'get_visible_jira_projects',
|
||||
'get_jira_project_issue_types_metadata',
|
||||
'get_jira_issue',
|
||||
'get_transitions_for_jira_issue',
|
||||
'get_jira_issue_remote_issue_links',
|
||||
'get_confluence_spaces',
|
||||
'get_confluence_page',
|
||||
'get_pages_in_confluence_space',
|
||||
'get_confluence_page_ancestors',
|
||||
'get_confluence_page_descendants',
|
||||
'get_confluence_page_footer_comments',
|
||||
'get_confluence_page_inline_comments',
|
||||
// Community Atlassian (sooperset/mcp-atlassian)
|
||||
'confluence_get_page',
|
||||
'confluence_get_page_children',
|
||||
'confluence_get_comments',
|
||||
'confluence_get_labels',
|
||||
'jira_get_issue',
|
||||
'jira_get_transitions',
|
||||
'jira_get_worklog',
|
||||
'jira_get_agile_boards',
|
||||
'jira_get_board_issues',
|
||||
'jira_get_sprints_from_board',
|
||||
'jira_get_sprint_issues',
|
||||
'jira_get_link_types',
|
||||
'jira_download_attachments',
|
||||
'jira_batch_get_changelogs',
|
||||
'jira_get_user_profile',
|
||||
'jira_get_project_issues',
|
||||
'jira_get_project_versions',
|
||||
// Asana (mcp.asana.com)
|
||||
'asana_get_attachment',
|
||||
'asana_get_attachments_for_object',
|
||||
'asana_get_goal',
|
||||
'asana_get_goals',
|
||||
'asana_get_parent_goals_for_goal',
|
||||
'asana_get_portfolio',
|
||||
'asana_get_portfolios',
|
||||
'asana_get_items_for_portfolio',
|
||||
'asana_get_project',
|
||||
'asana_get_projects',
|
||||
'asana_get_project_sections',
|
||||
'asana_get_project_status',
|
||||
'asana_get_project_statuses',
|
||||
'asana_get_project_task_counts',
|
||||
'asana_get_projects_for_team',
|
||||
'asana_get_projects_for_workspace',
|
||||
'asana_get_task',
|
||||
'asana_get_tasks',
|
||||
'asana_get_stories_for_task',
|
||||
'asana_get_teams_for_workspace',
|
||||
'asana_get_teams_for_user',
|
||||
'asana_get_team_users',
|
||||
'asana_get_time_period',
|
||||
'asana_get_time_periods',
|
||||
'asana_get_user',
|
||||
'asana_get_workspace_users',
|
||||
'asana_list_workspaces',
|
||||
// Filesystem (@modelcontextprotocol/server-filesystem)
|
||||
'read_file',
|
||||
'read_text_file',
|
||||
'read_media_file',
|
||||
'read_multiple_files',
|
||||
'list_directory',
|
||||
'list_directory_with_sizes',
|
||||
'directory_tree',
|
||||
'get_file_info',
|
||||
'list_allowed_directories',
|
||||
// Memory (@modelcontextprotocol/server-memory)
|
||||
'read_graph',
|
||||
'open_nodes',
|
||||
// Postgres (@modelcontextprotocol/server-postgres)
|
||||
'query',
|
||||
// SQLite (@modelcontextprotocol/server-sqlite)
|
||||
'read_query',
|
||||
'list_tables',
|
||||
'describe_table',
|
||||
// Git (mcp-server-git)
|
||||
'git_status',
|
||||
'git_diff',
|
||||
'git_diff_unstaged',
|
||||
'git_diff_staged',
|
||||
'git_log',
|
||||
'git_show',
|
||||
'git_branch',
|
||||
// Grafana (grafana/mcp-grafana)
|
||||
'list_teams',
|
||||
'list_users_by_org',
|
||||
'get_dashboard_by_uid',
|
||||
'get_dashboard_summary',
|
||||
'get_dashboard_property',
|
||||
'get_dashboard_panel_queries',
|
||||
'run_panel_query',
|
||||
'list_datasources',
|
||||
'get_datasource',
|
||||
'get_query_examples',
|
||||
'query_prometheus',
|
||||
'query_prometheus_histogram',
|
||||
'list_prometheus_metric_metadata',
|
||||
'list_prometheus_metric_names',
|
||||
'list_prometheus_label_names',
|
||||
'list_prometheus_label_values',
|
||||
'query_loki_logs',
|
||||
'query_loki_stats',
|
||||
'query_loki_patterns',
|
||||
'list_loki_label_names',
|
||||
'list_loki_label_values',
|
||||
'list_incidents',
|
||||
'get_incident',
|
||||
'list_sift_investigations',
|
||||
'get_sift_investigation',
|
||||
'get_sift_analysis',
|
||||
'list_oncall_schedules',
|
||||
'get_oncall_shift',
|
||||
'get_current_oncall_users',
|
||||
'list_oncall_teams',
|
||||
'list_oncall_users',
|
||||
'list_alert_groups',
|
||||
'get_alert_group',
|
||||
'get_annotations',
|
||||
'get_annotation_tags',
|
||||
'get_panel_image',
|
||||
// PagerDuty (PagerDuty/pagerduty-mcp-server)
|
||||
'list_incidents',
|
||||
'get_incident',
|
||||
'get_outlier_incident',
|
||||
'get_past_incidents',
|
||||
'get_related_incidents',
|
||||
'list_incident_notes',
|
||||
'list_incident_workflows',
|
||||
'get_incident_workflow',
|
||||
'list_services',
|
||||
'get_service',
|
||||
'list_team_members',
|
||||
'get_user_data',
|
||||
'list_schedules',
|
||||
'get_schedule',
|
||||
'list_schedule_users',
|
||||
'list_oncalls',
|
||||
'list_log_entries',
|
||||
'get_log_entry',
|
||||
'list_escalation_policies',
|
||||
'get_escalation_policy',
|
||||
'list_event_orchestrations',
|
||||
'get_event_orchestration',
|
||||
'list_status_pages',
|
||||
'get_status_page_post',
|
||||
'list_alerts_from_incident',
|
||||
'get_alert_from_incident',
|
||||
'list_change_events',
|
||||
'get_change_event',
|
||||
// Supabase (supabase-community/supabase-mcp)
|
||||
'list_organizations',
|
||||
'get_organization',
|
||||
'get_cost',
|
||||
'list_extensions',
|
||||
'list_migrations',
|
||||
'get_logs',
|
||||
'get_advisors',
|
||||
'get_project_url',
|
||||
'get_publishable_keys',
|
||||
'generate_typescript_types',
|
||||
'list_edge_functions',
|
||||
'get_edge_function',
|
||||
'list_storage_buckets',
|
||||
'get_storage_config',
|
||||
// Stripe (stripe/agent-toolkit)
|
||||
'get_stripe_account_info',
|
||||
'retrieve_balance',
|
||||
'list_customers',
|
||||
'list_products',
|
||||
'list_prices',
|
||||
'list_invoices',
|
||||
'list_payment_intents',
|
||||
'list_subscriptions',
|
||||
'list_coupons',
|
||||
'list_disputes',
|
||||
'fetch_stripe_resources',
|
||||
// PubMed (claude.ai hosted + community)
|
||||
'get_article_metadata',
|
||||
'get_full_text_article',
|
||||
'convert_article_ids',
|
||||
'get_copyright_status',
|
||||
'download_paper',
|
||||
'list_papers',
|
||||
'read_paper',
|
||||
'get_paper_fulltext',
|
||||
'get_pubmed_article_metadata',
|
||||
'download_pubmed_pdf',
|
||||
'pubmed_fetch',
|
||||
'pubmed_pmc_fetch',
|
||||
'pubmed_spell',
|
||||
'pubmed_cite',
|
||||
'pubmed_related',
|
||||
// BigQuery (claude.ai hosted + community)
|
||||
'bigquery_query',
|
||||
'bigquery_schema',
|
||||
'list_dataset_ids',
|
||||
'list_table_ids',
|
||||
'get_dataset_info',
|
||||
'get_table_info',
|
||||
// Firecrawl
|
||||
'firecrawl_scrape',
|
||||
'firecrawl_map',
|
||||
'firecrawl_crawl',
|
||||
'firecrawl_check_crawl_status',
|
||||
'firecrawl_extract',
|
||||
// Exa
|
||||
'get_code_context_exa',
|
||||
'company_research_exa',
|
||||
'crawling_exa',
|
||||
'deep_researcher_check',
|
||||
// Perplexity
|
||||
'perplexity_ask',
|
||||
'perplexity_research',
|
||||
'perplexity_reason',
|
||||
// Tavily
|
||||
'tavily_extract',
|
||||
'tavily_crawl',
|
||||
'tavily_map',
|
||||
'tavily_research',
|
||||
// Obsidian (MarkusPfundstein)
|
||||
'obsidian_list_files_in_vault',
|
||||
'obsidian_list_files_in_dir',
|
||||
'obsidian_get_file_contents',
|
||||
'obsidian_batch_get_file_contents',
|
||||
'obsidian_get_periodic_note',
|
||||
'obsidian_get_recent_periodic_notes',
|
||||
'obsidian_get_recent_changes',
|
||||
// Figma (GLips/Figma-Context-MCP)
|
||||
'get_figma_data',
|
||||
'download_figma_images',
|
||||
// Playwright (microsoft/playwright-mcp)
|
||||
'browser_console_messages',
|
||||
'browser_network_requests',
|
||||
'browser_take_screenshot',
|
||||
'browser_snapshot',
|
||||
'browser_get_config',
|
||||
'browser_route_list',
|
||||
'browser_cookie_list',
|
||||
'browser_cookie_get',
|
||||
'browser_localstorage_list',
|
||||
'browser_localstorage_get',
|
||||
'browser_sessionstorage_list',
|
||||
'browser_sessionstorage_get',
|
||||
'browser_storage_state',
|
||||
// Puppeteer (@modelcontextprotocol/server-puppeteer)
|
||||
'puppeteer_screenshot',
|
||||
// MongoDB
|
||||
'list_databases',
|
||||
'list_collections',
|
||||
'collection_indexes',
|
||||
'collection_schema',
|
||||
'collection_storage_size',
|
||||
'db_stats',
|
||||
'explain',
|
||||
'mongodb_logs',
|
||||
'aggregate',
|
||||
'count',
|
||||
'export',
|
||||
// Neo4j
|
||||
'get_neo4j_schema',
|
||||
'read_neo4j_cypher',
|
||||
'list_instances',
|
||||
'get_instance_details',
|
||||
'get_instance_by_name',
|
||||
// Elasticsearch (elastic)
|
||||
'list_indices',
|
||||
'get_mappings',
|
||||
'esql',
|
||||
'get_shards',
|
||||
// Airtable
|
||||
'list_records',
|
||||
'list_bases',
|
||||
'get_record',
|
||||
// Todoist (Doist — kebab-case, normalized)
|
||||
'get_productivity_stats',
|
||||
'get_overview',
|
||||
'fetch_object',
|
||||
'user_info',
|
||||
'list_workspaces',
|
||||
'view_attachment',
|
||||
// AWS (awslabs/mcp)
|
||||
'get_available_services',
|
||||
'read_documentation',
|
||||
'read_sections',
|
||||
'recommend',
|
||||
'analyze_log_group',
|
||||
'analyze_metric',
|
||||
'describe_log_groups',
|
||||
'get_active_alarms',
|
||||
'get_alarm_history',
|
||||
'get_metric_data',
|
||||
'get_metric_metadata',
|
||||
// Kubernetes
|
||||
'kubectl_get',
|
||||
'kubectl_describe',
|
||||
'kubectl_logs',
|
||||
'kubectl_context',
|
||||
'explain_resource',
|
||||
'list_api_resources',
|
||||
'namespaces_list',
|
||||
'nodes_log',
|
||||
'nodes_top',
|
||||
'pods_get',
|
||||
'pods_list',
|
||||
'pods_list_in_namespace',
|
||||
'pods_log',
|
||||
'pods_top',
|
||||
'resources_get',
|
||||
'resources_list',
|
||||
])
|
||||
|
||||
function normalize(name: string): string {
|
||||
return name
|
||||
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
||||
.replace(/-/g, '_')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
export function classifyMcpToolForCollapse(
|
||||
_serverName: string,
|
||||
toolName: string,
|
||||
): { isSearch: boolean; isRead: boolean } {
|
||||
const normalized = normalize(toolName)
|
||||
return {
|
||||
isSearch: SEARCH_TOOLS.has(normalized),
|
||||
isRead: READ_TOOLS.has(normalized),
|
||||
}
|
||||
}
|
||||
3
packages/builtin-tools/src/tools/MCPTool/prompt.ts
Normal file
3
packages/builtin-tools/src/tools/MCPTool/prompt.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Actual prompt and description are overridden in mcpClient.ts
|
||||
export const PROMPT = ''
|
||||
export const DESCRIPTION = ''
|
||||
Reference in New Issue
Block a user