mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-09 02:09:12 +00:00
feat: mcp resources (#6542)
This commit is contained in:
@@ -28,6 +28,17 @@ export namespace MCP {
|
||||
const log = Log.create({ service: "mcp" })
|
||||
const DEFAULT_TIMEOUT = 5000
|
||||
|
||||
export const Resource = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
uri: z.string(),
|
||||
description: z.string().optional(),
|
||||
mimeType: z.string().optional(),
|
||||
client: z.string(),
|
||||
})
|
||||
.meta({ ref: "McpResource" })
|
||||
export type Resource = z.infer<typeof Resource>
|
||||
|
||||
export const ToolsChanged = BusEvent.define(
|
||||
"mcp.tools.changed",
|
||||
z.object({
|
||||
@@ -136,6 +147,7 @@ export namespace MCP {
|
||||
// Prompt cache types
|
||||
type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][number]
|
||||
|
||||
type ResourceInfo = Awaited<ReturnType<MCPClient["listResources"]>>["resources"][number]
|
||||
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
|
||||
function isMcpConfigured(entry: McpEntry): entry is Config.Mcp {
|
||||
return typeof entry === "object" && entry !== null && "type" in entry
|
||||
@@ -213,6 +225,28 @@ export namespace MCP {
|
||||
return commands
|
||||
}
|
||||
|
||||
async function fetchResourcesForClient(clientName: string, client: Client) {
|
||||
const resources = await client.listResources().catch((e) => {
|
||||
log.error("failed to get prompts", { clientName, error: e.message })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!resources) {
|
||||
return
|
||||
}
|
||||
|
||||
const commands: Record<string, ResourceInfo & { client: string }> = {}
|
||||
|
||||
for (const resource of resources.resources) {
|
||||
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const sanitizedResourceName = resource.name.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const key = sanitizedClientName + ":" + sanitizedResourceName
|
||||
|
||||
commands[key] = { ...resource, client: clientName }
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
export async function add(name: string, mcp: Config.Mcp) {
|
||||
const s = await state()
|
||||
const result = await create(name, mcp)
|
||||
@@ -559,6 +593,27 @@ export namespace MCP {
|
||||
return prompts
|
||||
}
|
||||
|
||||
export async function resources() {
|
||||
const s = await state()
|
||||
const clientsSnapshot = await clients()
|
||||
|
||||
const result = Object.fromEntries<ResourceInfo & { client: string }>(
|
||||
(
|
||||
await Promise.all(
|
||||
Object.entries(clientsSnapshot).map(async ([clientName, client]) => {
|
||||
if (s.status[clientName]?.status !== "connected") {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries((await fetchResourcesForClient(clientName, client)) ?? {})
|
||||
}),
|
||||
)
|
||||
).flat(),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getPrompt(clientName: string, name: string, args?: Record<string, string>) {
|
||||
const clientsSnapshot = await clients()
|
||||
const client = clientsSnapshot[clientName]
|
||||
@@ -587,6 +642,33 @@ export namespace MCP {
|
||||
return result
|
||||
}
|
||||
|
||||
export async function readResource(clientName: string, resourceUri: string) {
|
||||
const clientsSnapshot = await clients()
|
||||
const client = clientsSnapshot[clientName]
|
||||
|
||||
if (!client) {
|
||||
log.warn("client not found for prompt", {
|
||||
clientName: clientName,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const result = await client
|
||||
.readResource({
|
||||
uri: resourceUri,
|
||||
})
|
||||
.catch((e) => {
|
||||
log.error("failed to get prompt from MCP server", {
|
||||
clientName: clientName,
|
||||
resourceUri: resourceUri,
|
||||
error: e.message,
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OAuth authentication flow for an MCP server.
|
||||
* Returns the authorization URL that should be opened in a browser.
|
||||
|
||||
Reference in New Issue
Block a user