NYTimes Article Search MCP Server
by angheljf
- nyt
- src
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
ErrorCode,
McpError
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import dotenv from "dotenv";
import {
NYTimesApiResponse,
ArticleSearchResult,
isValidSearchArticlesArgs
} from "./types.js";
dotenv.config();
const API_KEY = process.env.NYTIMES_API_KEY;
if (!API_KEY) {
throw new Error("NYTIMES_API_KEY environment variable is required");
}
const API_CONFIG = {
BASE_URL: 'https://api.nytimes.com/svc/search/v2',
ENDPOINT: 'articlesearch.json'
} as const;
class NYTimesServer {
private server: Server;
private axiosInstance;
constructor() {
this.server = new Server({
name: "nytimes-article-search-server",
version: "0.1.0"
}, {
capabilities: {
resources: {},
tools: {}
}
});
this.axiosInstance = axios.create({
baseURL: API_CONFIG.BASE_URL,
params: {
'api-key': API_KEY
}
});
this.setupHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupHandlers(): void {
this.setupResourceHandlers();
this.setupToolHandlers();
}
private setupResourceHandlers(): void {
this.server.setRequestHandler(
ListResourcesRequestSchema,
async () => ({
resources: [] // No static resources for this server
})
);
this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown resource: ${request.params.uri}`
);
}
);
}
private setupToolHandlers(): void {
this.server.setRequestHandler(
ListToolsRequestSchema,
async () => ({
tools: [{
name: "search_articles",
description: "Search NYTimes articles from the last 30 days based on a keyword",
inputSchema: {
type: "object",
properties: {
keyword: {
type: "string",
description: "Keyword to search for in articles"
}
},
required: ["keyword"]
}
}]
})
);
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
if (request.params.name !== "search_articles") {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
if (!isValidSearchArticlesArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid search arguments"
);
}
const keyword = request.params.arguments.keyword;
try {
const response = await this.axiosInstance.get<NYTimesApiResponse>(API_CONFIG.ENDPOINT, {
params: {
q: keyword,
sort: 'newest',
'begin_date': this.getDateString(30), // 30 days ago
'end_date': this.getDateString(0) // today
}
});
const articles: ArticleSearchResult[] = response.data.response.docs.map(article => ({
title: article.headline.main,
abstract: article.abstract,
url: article.web_url,
publishedDate: article.pub_date,
author: article.byline.original || 'Unknown'
}));
return {
content: [{
type: "text",
text: JSON.stringify(articles, null, 2)
}]
};
} catch (error) {
if (axios.isAxiosError(error)) {
return {
content: [{
type: "text",
text: `NYTimes API error: ${error.response?.data.message ?? error.message}`
}],
isError: true,
}
}
throw error;
}
}
);
}
private getDateString(daysAgo: number): string {
const date = new Date();
date.setDate(date.getDate() - daysAgo);
return date.toISOString().split('T')[0].replace(/-/g, '');
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("NYTimes MCP server running on stdio");
}
}
const server = new NYTimesServer();
server.run().catch(console.error);