init claude-code
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { z } from 'zod/v4'
|
||||
import {
|
||||
ensureConnectedClient,
|
||||
fetchResourcesForClient,
|
||||
} from '../../services/mcp/client.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { logMCPError } from '../../utils/log.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { isOutputLineTruncated } from '../../utils/terminal.js'
|
||||
import { DESCRIPTION, LIST_MCP_RESOURCES_TOOL_NAME, PROMPT } from './prompt.js'
|
||||
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
server: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional server name to filter resources by'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.array(
|
||||
z.object({
|
||||
uri: z.string().describe('Resource URI'),
|
||||
name: z.string().describe('Resource name'),
|
||||
mimeType: z.string().optional().describe('MIME type of the resource'),
|
||||
description: z.string().optional().describe('Resource description'),
|
||||
server: z.string().describe('Server that provides this resource'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
export const ListMcpResourcesTool = buildTool({
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
toAutoClassifierInput(input) {
|
||||
return input.server ?? ''
|
||||
},
|
||||
shouldDefer: true,
|
||||
name: LIST_MCP_RESOURCES_TOOL_NAME,
|
||||
searchHint: 'list resources from connected MCP servers',
|
||||
maxResultSizeChars: 100_000,
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
async prompt() {
|
||||
return PROMPT
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
async call(input, { options: { mcpClients } }) {
|
||||
const { server: targetServer } = input
|
||||
|
||||
const clientsToProcess = targetServer
|
||||
? mcpClients.filter(client => client.name === targetServer)
|
||||
: mcpClients
|
||||
|
||||
if (targetServer && clientsToProcess.length === 0) {
|
||||
throw new Error(
|
||||
`Server "${targetServer}" not found. Available servers: ${mcpClients.map(c => c.name).join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
// fetchResourcesForClient is LRU-cached (by server name) and already
|
||||
// warm from startup prefetch. Cache is invalidated on onclose and on
|
||||
// resources/list_changed notifications, so results are never stale.
|
||||
// ensureConnectedClient is a no-op when healthy (memoize hit), but after
|
||||
// onclose it returns a fresh connection so the re-fetch succeeds.
|
||||
const results = await Promise.all(
|
||||
clientsToProcess.map(async client => {
|
||||
if (client.type !== 'connected') return []
|
||||
try {
|
||||
const fresh = await ensureConnectedClient(client)
|
||||
return await fetchResourcesForClient(fresh)
|
||||
} catch (error) {
|
||||
// One server's reconnect failure shouldn't sink the whole result.
|
||||
logMCPError(client.name, errorMessage(error))
|
||||
return []
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
data: results.flat(),
|
||||
}
|
||||
},
|
||||
renderToolUseMessage,
|
||||
userFacingName: () => 'listMcpResources',
|
||||
renderToolResultMessage,
|
||||
isResultTruncated(output: Output): boolean {
|
||||
return isOutputLineTruncated(jsonStringify(output))
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(content, toolUseID) {
|
||||
if (!content || content.length === 0) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content:
|
||||
'No resources found. MCP servers may still provide tools even if they have no resources.',
|
||||
}
|
||||
}
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: jsonStringify(content),
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
||||
import { OutputLine } from '../../components/shell/OutputLine.js';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { ToolProgressData } from '../../Tool.js';
|
||||
import type { ProgressMessage } from '../../types/message.js';
|
||||
import { jsonStringify } from '../../utils/slowOperations.js';
|
||||
import type { Output } from './ListMcpResourcesTool.js';
|
||||
export function renderToolUseMessage(input: Partial<{
|
||||
server?: string;
|
||||
}>): React.ReactNode {
|
||||
return input.server ? `List MCP resources from server "${input.server}"` : `List all MCP resources`;
|
||||
}
|
||||
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
|
||||
verbose
|
||||
}: {
|
||||
verbose: boolean;
|
||||
}): React.ReactNode {
|
||||
if (!output || output.length === 0) {
|
||||
return <MessageResponse height={1}>
|
||||
<Text dimColor>(No resources found)</Text>
|
||||
</MessageResponse>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- human-facing UI, not tool_result
|
||||
const formattedOutput = jsonStringify(output, null, 2);
|
||||
return <OutputLine content={formattedOutput} verbose={verbose} />;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1lc3NhZ2VSZXNwb25zZSIsIk91dHB1dExpbmUiLCJUZXh0IiwiVG9vbFByb2dyZXNzRGF0YSIsIlByb2dyZXNzTWVzc2FnZSIsImpzb25TdHJpbmdpZnkiLCJPdXRwdXQiLCJyZW5kZXJUb29sVXNlTWVzc2FnZSIsImlucHV0IiwiUGFydGlhbCIsInNlcnZlciIsIlJlYWN0Tm9kZSIsInJlbmRlclRvb2xSZXN1bHRNZXNzYWdlIiwib3V0cHV0IiwiX3Byb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlIiwidmVyYm9zZSIsImxlbmd0aCIsImZvcm1hdHRlZE91dHB1dCJdLCJzb3VyY2VzIjpbIlVJLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IE1lc3NhZ2VSZXNwb25zZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvTWVzc2FnZVJlc3BvbnNlLmpzJ1xuaW1wb3J0IHsgT3V0cHV0TGluZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvc2hlbGwvT3V0cHV0TGluZS5qcydcbmltcG9ydCB7IFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xQcm9ncmVzc0RhdGEgfSBmcm9tICcuLi8uLi9Ub29sLmpzJ1xuaW1wb3J0IHR5cGUgeyBQcm9ncmVzc01lc3NhZ2UgfSBmcm9tICcuLi8uLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHsganNvblN0cmluZ2lmeSB9IGZyb20gJy4uLy4uL3V0aWxzL3Nsb3dPcGVyYXRpb25zLmpzJ1xuaW1wb3J0IHR5cGUgeyBPdXRwdXQgfSBmcm9tICcuL0xpc3RNY3BSZXNvdXJjZXNUb29sLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZU1lc3NhZ2UoXG4gIGlucHV0OiBQYXJ0aWFsPHsgc2VydmVyPzogc3RyaW5nIH0+LFxuKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIGlucHV0LnNlcnZlclxuICAgID8gYExpc3QgTUNQIHJlc291cmNlcyBmcm9tIHNlcnZlciBcIiR7aW5wdXQuc2VydmVyfVwiYFxuICAgIDogYExpc3QgYWxsIE1DUCByZXNvdXJjZXNgXG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sUmVzdWx0TWVzc2FnZShcbiAgb3V0cHV0OiBPdXRwdXQsXG4gIF9wcm9ncmVzc01lc3NhZ2VzRm9yTWVzc2FnZTogUHJvZ3Jlc3NNZXNzYWdlPFRvb2xQcm9ncmVzc0RhdGE+W10sXG4gIHsgdmVyYm9zZSB9OiB7IHZlcmJvc2U6IGJvb2xlYW4gfSxcbik6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGlmICghb3V0cHV0IHx8IG91dHB1dC5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm4gKFxuICAgICAgPE1lc3NhZ2VSZXNwb25zZSBoZWlnaHQ9ezF9PlxuICAgICAgICA8VGV4dCBkaW1Db2xvcj4oTm8gcmVzb3VyY2VzIGZvdW5kKTwvVGV4dD5cbiAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgIClcbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1yZXN0cmljdGVkLXN5bnRheCAtLSBodW1hbi1mYWNpbmcgVUksIG5vdCB0b29sX3Jlc3VsdFxuICBjb25zdCBmb3JtYXR0ZWRPdXRwdXQgPSBqc29uU3RyaW5naWZ5KG91dHB1dCwgbnVsbCwgMilcblxuICByZXR1cm4gPE91dHB1dExpbmUgY29udGVudD17Zm9ybWF0dGVkT3V0cHV0fSB2ZXJib3NlPXt2ZXJib3NlfSAvPlxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLGVBQWUsUUFBUSxxQ0FBcUM7QUFDckUsU0FBU0MsVUFBVSxRQUFRLHNDQUFzQztBQUNqRSxTQUFTQyxJQUFJLFFBQVEsY0FBYztBQUNuQyxjQUFjQyxnQkFBZ0IsUUFBUSxlQUFlO0FBQ3JELGNBQWNDLGVBQWUsUUFBUSx3QkFBd0I7QUFDN0QsU0FBU0MsYUFBYSxRQUFRLCtCQUErQjtBQUM3RCxjQUFjQyxNQUFNLFFBQVEsMkJBQTJCO0FBRXZELE9BQU8sU0FBU0Msb0JBQW9CQSxDQUNsQ0MsS0FBSyxFQUFFQyxPQUFPLENBQUM7RUFBRUMsTUFBTSxDQUFDLEVBQUUsTUFBTTtBQUFDLENBQUMsQ0FBQyxDQUNwQyxFQUFFWCxLQUFLLENBQUNZLFNBQVMsQ0FBQztFQUNqQixPQUFPSCxLQUFLLENBQUNFLE1BQU0sR0FDZixtQ0FBbUNGLEtBQUssQ0FBQ0UsTUFBTSxHQUFHLEdBQ2xELHdCQUF3QjtBQUM5QjtBQUVBLE9BQU8sU0FBU0UsdUJBQXVCQSxDQUNyQ0MsTUFBTSxFQUFFUCxNQUFNLEVBQ2RRLDJCQUEyQixFQUFFVixlQUFlLENBQUNELGdCQUFnQixDQUFDLEVBQUUsRUFDaEU7RUFBRVk7QUFBOEIsQ0FBckIsRUFBRTtFQUFFQSxPQUFPLEVBQUUsT0FBTztBQUFDLENBQUMsQ0FDbEMsRUFBRWhCLEtBQUssQ0FBQ1ksU0FBUyxDQUFDO0VBQ2pCLElBQUksQ0FBQ0UsTUFBTSxJQUFJQSxNQUFNLENBQUNHLE1BQU0sS0FBSyxDQUFDLEVBQUU7SUFDbEMsT0FDRSxDQUFDLGVBQWUsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDakMsUUFBUSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsb0JBQW9CLEVBQUUsSUFBSTtBQUNqRCxNQUFNLEVBQUUsZUFBZSxDQUFDO0VBRXRCOztFQUVBO0VBQ0EsTUFBTUMsZUFBZSxHQUFHWixhQUFhLENBQUNRLE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0VBRXRELE9BQU8sQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUNJLGVBQWUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDRixPQUFPLENBQUMsR0FBRztBQUNuRSIsImlnbm9yZUxpc3QiOltdfQ==
|
||||
@@ -0,0 +1,20 @@
|
||||
export const LIST_MCP_RESOURCES_TOOL_NAME = 'ListMcpResourcesTool'
|
||||
|
||||
export const DESCRIPTION = `
|
||||
Lists available resources from configured MCP servers.
|
||||
Each resource object includes a 'server' field indicating which server it's from.
|
||||
|
||||
Usage examples:
|
||||
- List all resources from all servers: \`listMcpResources\`
|
||||
- List resources from a specific server: \`listMcpResources({ server: "myserver" })\`
|
||||
`
|
||||
|
||||
export const PROMPT = `
|
||||
List available resources from configured MCP servers.
|
||||
Each returned resource will include all standard MCP resource fields plus a 'server' field
|
||||
indicating which server the resource belongs to.
|
||||
|
||||
Parameters:
|
||||
- server (optional): The name of a specific MCP server to get resources from. If not provided,
|
||||
resources from all servers will be returned.
|
||||
`
|
||||
Reference in New Issue
Block a user