MCP Tools for Obsidian
by jacksteamdev
import { logger, type ToolRegistry } from "$/shared";
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import { type } from "arktype";
import { DEFAULT_USER_AGENT } from "./constants";
import { convertHtmlToMarkdown } from "./services/markdown";
export function registerFetchTool(tools: ToolRegistry, server: Server) {
tools.register(
type({
name: '"fetch"',
arguments: {
url: "string",
"maxLength?": type("number").describe("Limit response length."),
"startIndex?": type("number").describe(
"Supports paginated retrieval of content.",
),
"raw?": type("boolean").describe(
"Returns raw HTML content if raw=true.",
),
},
}).describe(
"Reads and returns the content of any web page. Returns the content in Markdown format by default, or can return raw HTML if raw=true parameter is set. Supports pagination through maxLength and startIndex parameters.",
),
async ({ arguments: args }) => {
logger.info("Fetching URL", { url: args.url });
try {
const response = await fetch(args.url, {
headers: {
"User-Agent": DEFAULT_USER_AGENT,
},
});
if (!response.ok) {
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch ${args.url} - status code ${response.status}`,
);
}
const contentType = response.headers.get("content-type") || "";
const text = await response.text();
const isHtml =
text.toLowerCase().includes("<html") ||
contentType.includes("text/html") ||
!contentType;
let content: string;
let prefix = "";
if (isHtml && !args.raw) {
content = convertHtmlToMarkdown(text, args.url);
} else {
content = text;
prefix = `Content type ${contentType} cannot be simplified to markdown, but here is the raw content:\n`;
}
const maxLength = args.maxLength || 5000;
const startIndex = args.startIndex || 0;
const totalLength = content.length;
if (totalLength > maxLength) {
content = content.substring(startIndex, startIndex + maxLength);
content += `\n\n<error>Content truncated. Call the fetch tool with a startIndex of ${
startIndex + maxLength
} to get more content.</error>`;
}
logger.debug("URL fetched successfully", {
url: args.url,
contentLength: content.length,
});
return {
content: [
{
type: "text",
text: `${prefix}Contents of ${args.url}:\n${content}`,
},
{
type: "text",
text: `Pagination: ${JSON.stringify({
totalLength,
startIndex,
endIndex: startIndex + content.length,
hasMore: true,
})}`,
},
],
};
} catch (error) {
logger.error("Failed to fetch URL", { url: args.url, error });
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch ${args.url}: ${error}`,
);
}
},
);
}