Skip to content

Instantly share code, notes, and snippets.

@JenHsuan
Last active November 30, 2025 03:48
Show Gist options
  • Select an option

  • Save JenHsuan/d1ba0679d6627abe1571831eeff67344 to your computer and use it in GitHub Desktop.

Select an option

Save JenHsuan/d1ba0679d6627abe1571831eeff67344 to your computer and use it in GitHub Desktop.
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Article interface based on the API structure
interface Article {
id: number;
title: string;
subtitle: string;
image: string;
url: string;
name: string;
time: string;
readtime: string;
category: number;
description: string;
shareCount: number;
checkCount: number;
}
// Cloudflare Workers environment bindings
interface Env {
ARTICLES_API_URL: string;
RATE_LIMITER: {
limit: (options: { key: string }) => Promise<{ success: boolean }>;
};
}
// Define our MCP agent for Alayman articles (exported as Durable Object)
class AlaymanMCP extends McpAgent<Env> {
server = new McpServer({
name: "Alayman Articles Server",
version: "1.0.0",
});
private async fetchArticles(): Promise<Article[]> {
try {
const apiUrl = (this.env as Env).ARTICLES_API_URL;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const articles = await response.json();
return articles as Article[];
} catch (error) {
console.error("Error fetching articles:", error);
throw error;
}
}
private formatArticle(article: Article): string {
return `ID: ${article.id}
Title: ${article.title}
Subtitle: ${article.subtitle}
Author: ${article.name}
Published: ${article.time}
Reading Time: ${article.readtime}
Category: ${article.category}
URL: ${article.url}
Image: ${article.image}
Shares: ${article.shareCount}
Checks: ${article.checkCount}
${article.description ? `Description: ${article.description}` : ""}`;
}
private formatArticles(articles: Article[]): string {
return articles.map((article) => this.formatArticle(article)).join("\n\n---\n\n");
}
async init() {
// Tool 1: Get all articles with pagination
this.server.tool(
"get_all_articles",
{
limit: z.number().int().positive().default(20).describe("Number of articles to return (default: 20)"),
offset: z.number().int().nonnegative().default(0).describe("Number of articles to skip (default: 0)"),
},
async ({ limit = 20, offset = 0 }) => {
try {
const articles = await this.fetchArticles();
const total = articles.length;
const paginatedArticles = articles.slice(offset, offset + limit);
const hasMore = offset + limit < total;
const response = {
articles: paginatedArticles,
total,
offset,
limit,
has_more: hasMore,
};
return {
content: [
{
type: "text",
text: JSON.stringify(response, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching articles: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
);
// Tool 2: Search articles by title keyword
this.server.tool(
"search_articles",
{
keyword: z.string().min(1).describe("Keyword to search in article titles"),
},
async ({ keyword }) => {
try {
const articles = await this.fetchArticles();
const searchTerm = keyword.toLowerCase();
const matchedArticles = articles.filter((article) =>
article.title.toLowerCase().includes(searchTerm),
);
if (matchedArticles.length === 0) {
return {
content: [
{
type: "text",
text: `No articles found matching keyword: "${keyword}"`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Found ${matchedArticles.length} article(s) matching "${keyword}":\n\n${this.formatArticles(matchedArticles)}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error searching articles: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
);
// Prompt: List articles with custom condition
this.server.registerPrompt(
"list_articles",
{
title: "list_alayman_articles",
description: "Generate a prompt to list a specific number of alayman's articles with custom conditions",
argsSchema: {
number: z.coerce.number().int().positive().default(10).describe("Number of articles to list (default: 10)"),
condition: z.string().optional().describe("Custom condition or filter criteria for the articles"),
},
},
async ({ number = 10, condition }) => {
const text = `List ${number} alayman's articles${condition ? ` ${condition}` : ""}`;
return {
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: text,
},
},
],
};
},
);
}
}
// Export the Durable Object class
export { AlaymanMCP };
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
// Extract rate limiting key from Mcp-session-id header
const sessionId = request.headers.get("Mcp-session-id") || request.headers.get('mcp-session-id') || "unknown";
// Apply rate limiting to all endpoints
const { success } = await env.RATE_LIMITER.limit({ key: sessionId });
if (!success) {
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: "You have exceeded the rate limit of 60 requests per 60 seconds. Please try again later.",
limit: 60,
period: 60,
}),
{
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": "60",
},
},
);
}
// SSE endpoints
if (url.pathname === "/sse" || url.pathname === "/sse/message") {
return AlaymanMCP.serveSSE("/sse").fetch(request, env, ctx);
}
// MCP endpoint
if (url.pathname === "/mcp") {
return AlaymanMCP.serve("/mcp").fetch(request, env, ctx);
}
// Health check / info endpoint
if (url.pathname === "/") {
return new Response(
JSON.stringify({
name: "Alayman MCP Server",
version: "1.0.0",
description: "MCP server for fetching articles from alayman.io",
endpoints: {
sse: "/sse",
mcp: "/mcp",
},
tools: ["get_all_articles", "search_articles"],
prompts: ["list_articles"],
rateLimit: {
limit: 60,
period: 60,
key: "Mcp-session-id header or IP address",
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response("Not found", { status: 404 });
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment