import { MCPManager } from "../mcp/manager";
import type { MCPResourceReadResult } from "../mcp/types";
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
function escapeRegex(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getUriTemplateMatchScore(
uri: string,
uriTemplate: string,
): { literalChars: number; expressionCount: number } | undefined {
const expressionPattern = /\{[^}]+\}/g;
const literalSegments = uriTemplate.split(expressionPattern);
const expressionCount = (uriTemplate.match(expressionPattern) ?? []).length;
const pattern = literalSegments.map(escapeRegex).join("(.*?)");
const regex = new RegExp(`^${pattern}$`);
if (!regex.test(uri)) return undefined;
const literalChars = literalSegments.reduce((total, segment) => total + segment.length, 0);
return { literalChars, expressionCount };
}
function extractResourceUri(url: InternalUrl): string {
const host = url.rawHost || url.hostname;
const rawPathname = url.rawPathname ?? url.pathname;
const hasPath = rawPathname && rawPathname !== "/";
const uri = `${host}${hasPath ? rawPathname : ""}${url.search}${url.hash}`.trim();
if (!uri) {
throw new Error("mcp:// URL requires a resource URI: mcp://<resource-uri>");
}
return uri;
}
function resolveTargetServer(mcpManager: MCPManager, uri: string): string | undefined {
const servers = mcpManager.getConnectedServers();
for (const name of servers) {
const serverResources = mcpManager.getServerResources(name);
if (serverResources?.resources.some(r => r.uri === uri)) {
return name;
}
}
let bestTemplateMatch:
| {
serverName: string;
literalChars: number;
expressionCount: number;
serverIndex: number;
templateIndex: number;
}
| undefined;
for (const [serverIndex, name] of servers.entries()) {
const serverResources = mcpManager.getServerResources(name);
if (!serverResources) continue;
for (const [templateIndex, template] of serverResources.templates.entries()) {
const match = getUriTemplateMatchScore(uri, template.uriTemplate);
if (!match) continue;
const isBetterMatch =
!bestTemplateMatch ||
match.literalChars > bestTemplateMatch.literalChars ||
(match.literalChars === bestTemplateMatch.literalChars &&
(match.expressionCount < bestTemplateMatch.expressionCount ||
(match.expressionCount === bestTemplateMatch.expressionCount &&
(serverIndex < bestTemplateMatch.serverIndex ||
(serverIndex === bestTemplateMatch.serverIndex &&
templateIndex < bestTemplateMatch.templateIndex)))));
if (isBetterMatch) {
bestTemplateMatch = {
serverName: name,
literalChars: match.literalChars,
expressionCount: match.expressionCount,
serverIndex,
templateIndex,
};
}
}
}
return bestTemplateMatch?.serverName;
}
function formatAvailableResources(mcpManager: MCPManager): string {
const available = mcpManager
.getConnectedServers()
.flatMap(name => {
const serverResources = mcpManager.getServerResources(name);
return (serverResources?.resources ?? []).map(r => ` ${r.uri} (${name})`);
})
.join("\n");
return available || " (none)";
}
* Protocol handler for mcp:// URLs.
*
* URL form:
* - mcp://<resource-uri> (e.g. mcp://test://notes, mcp://ibkr://portfolio/positions)
*/
export class McpProtocolHandler implements ProtocolHandler {
readonly scheme = "mcp";
readonly immutable = true;
async resolve(url: InternalUrl): Promise<InternalResource> {
const mcpManager = MCPManager.instance();
if (!mcpManager) {
throw new Error("No MCP manager available. MCP servers may not be configured.");
}
const uri = extractResourceUri(url);
const targetServer = resolveTargetServer(mcpManager, uri);
if (!targetServer) {
throw new Error(
`No MCP server has resource "${uri}".\n\nAvailable resources:\n${formatAvailableResources(mcpManager)}`,
);
}
let result: MCPResourceReadResult | undefined;
try {
result = await mcpManager.readServerResource(targetServer, uri);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`MCP resource read error: ${message}`);
}
if (!result) {
throw new Error(`Server "${targetServer}" returned no content for "${uri}".`);
}
const textParts: string[] = [];
for (const item of result.contents) {
if (item.text !== undefined && item.text !== null) {
textParts.push(item.text);
} else if (item.blob) {
textParts.push(`[Binary content: ${item.mimeType ?? "unknown"}, base64 length ${item.blob.length}]`);
}
}
const content = textParts.length > 0 ? textParts.join("\n---\n") : "(empty resource)";
return {
url: url.href,
content,
contentType: "text/plain",
size: Buffer.byteLength(content, "utf-8"),
notes: [`MCP server: ${targetServer}`],
};
}
}