Skip to content

Instantly share code, notes, and snippets.

@laiso
Last active April 27, 2025 06:02
Show Gist options
  • Save laiso/8e45236886bd24dc865dde557db11465 to your computer and use it in GitHub Desktop.
Save laiso/8e45236886bd24dc865dde557db11465 to your computer and use it in GitHub Desktop.
tltr MCP Server on Cloudflare Workers
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { GoogleGenerativeAI } from "@google/generative-ai";
import { Readability } from '@mozilla/readability';
import { parseHTML } from 'linkedom';
type Env = {
MyMCP: DurableObjectNamespace<MyMCP>;
GEMINI_API_KEY: string;
};
type State = {};
export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "tltr MCP Server",
description: "Summarize lengthy articles. Fetch content from a URL and provide a concise summary.",
version: "1.0.0",
});
initialState: State = {};
async init() {
this.server.tool(
"tltr",
"Summarize lengthy articles. Fetch content from a URL and provide a concise summary.",
{ url: z.string().url() },
async ({ url }) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.statusText}`);
}
const html = await response.text();
const textContent = extractTextFromHTML(html);
if (!textContent) {
return { content: [{ type: "text", text: "Could not extract text content from the URL." }] };
}
const apiKey = this.env.GEMINI_API_KEY;
const { pageTitle, summary } = await summarizeContent(textContent, apiKey);
return {
content: [
{ type: "text", text: `**${pageTitle}**\n\n${summary}` },
],
};
} catch (error) {
console.error("Error in tltr tool:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{ type: "text", text: `Error processing URL: ${errorMessage}` },
],
};
}
}
);
}
onStateUpdate(state: State) {
console.log({ stateUpdate: state });
}
}
export default MyMCP.mount("/sse", {
binding: "MyMCP",
})
export function extractTextFromHTML(html: string): string {
try {
const { document } = parseHTML(html);
const reader = new Readability(document);
const article = reader.parse();
if (!article) return "";
return `${article.title}\n\n${article.textContent}` || "";
} catch (error) {
console.error("Failed to extract content using Readability:", error);
return "";
}
}
export async function summarizeContent(content: string, apiKey: string): Promise<{ pageTitle: string, summary: string }> {
if (!apiKey) throw new Error("Gemini API key not configured");
const genAI = new GoogleGenerativeAI(apiKey);
const systemInstruction = `Summarize the user's input.
## Instructions
- Start with a one-word conclusion.
- Then summarize the content in bullet points.
- Finally, generate a table of contents.
## Rules
- Start directly with the conclusion → summary without any preamble.
- Do not use headings like "Summary" in the response.
- Ensure no information is lost in the summary.
`;
const generationConfig = {
temperature: 0,
topP: 0.95,
topK: 40,
maxOutputTokens: 4192,
responseMimeType: "text/plain",
};
const model = genAI.getGenerativeModel({
model: "gemini-2.5-pro-exp-03-25",
systemInstruction,
generationConfig
});
const result = await model.generateContent(content);
const summaryText = result.response.text();
const contentLines = content.split('\n');
const potentialTitle = contentLines.length > 0 ? contentLines[0].trim() : "Summary";
const pageTitle = potentialTitle || "Summary";
const summary = summaryText.trim();
return { pageTitle, summary };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment