index.ts•3.03 kB
#!/usr/bin/env bun
import { z } from "zod";
import pkg from "./package.json";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// Define the schema for the component's input using Zod.
const InputSchema = z.object({
url: z.string().url({ message: "A valid URL is required." }),
});
// Define the schema for the component's successful output.
const OutputSchema = z.object({
content: z.array(
z.object({
type: z.literal("text"),
text: z.string(),
}),
),
});
/**
* The core logic for reading webpage content.
* This function is now cleaner, without direct console logging.
* It focuses on doing one thing: fetching the content or throwing an error.
* @param url The URL of the webpage to read.
* @returns A promise that resolves to the plain text content of the page.
*/
export async function readWebpageContent(url: string): Promise<string> {
const jinaUrl = `https://r.jina.ai/${url}`;
const response = await fetch(jinaUrl, {
headers: {
Accept: "text/plain",
},
});
if (!response.ok) {
throw new Error(
`Jina Reader API request failed with status ${response.status}: ${response.statusText}`,
);
}
const content = await response.text();
// Case 1: Structured JSON error from Jina.
try {
const jsonData = JSON.parse(content);
if (jsonData && jsonData.name && jsonData.code && jsonData.message) {
throw new Error(
`Jina API Error: ${jsonData.readableMessage || jsonData.message}`,
);
}
} catch (e) {
if (!(e instanceof SyntaxError)) {
throw e;
}
}
// Case 2: Empty or minimal HTML content.
const textOnly = content.replace(/<[^>]*>/g, "").trim();
if (!textOnly) {
throw new Error(
"Jina Reader returned empty or minimal content, likely due to an upstream error (e.g., 404 Not Found).",
);
}
return content;
}
// Create a server instance with basic metadata.
const server = new McpServer({
name: "jina-free-mcp",
version: pkg.version,
});
// Define the tool for reading a webpage.
server.tool(
"read-webpage",
"Reads the content of a given URL using the Jina Reader API.",
{
input: InputSchema,
output: OutputSchema,
},
async (args: { input: z.infer<typeof InputSchema> }) => {
// Execute the core logic with the validated input.
const { input } = args;
const content = await readWebpageContent(input.url);
// Return the result in the standard MCP content format.
return {
content: [
{
type: "text",
text: content,
},
],
};
},
);
// Main execution block to connect the server to a transport.
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server 'jina-free-mcp' is running on stdio.");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});