├── .gitignore ├── img └── example.png ├── tsconfig.json ├── smithery.yaml ├── package.json ├── Dockerfile ├── LICENSE ├── src ├── types.ts └── index.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* -------------------------------------------------------------------------------- /img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angheljf/nyt/HEAD/img/example.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - nytimesApiKey 10 | properties: 11 | nytimesApiKey: 12 | type: string 13 | description: The API key for accessing the New York Times API. 14 | commandFunction: 15 | # A function that produces the CLI command to start the MCP on stdio. 16 | |- 17 | (config) => ({command:'node',args:['build/index.js'],env:{NYTIMES_API_KEY:config.nytimesApiKey}}) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nyt", 3 | "version": "0.1.0", 4 | "description": "A MCP Server", 5 | "private": true, 6 | "type": "module", 7 | "bin": { 8 | "nyt": "./build/index.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 15 | "prepare": "npm run build", 16 | "watch": "tsc --watch", 17 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 18 | }, 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "0.6.0", 21 | "axios": "^1.7.8", 22 | "dotenv": "^16.4.5" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.11.24", 26 | "typescript": "^5.3.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Base image for building TypeScript 3 | FROM node:20-alpine AS builder 4 | 5 | # Set working directory 6 | WORKDIR /app 7 | 8 | # Copy package.json and package-lock.json 9 | COPY package.json package-lock.json ./ 10 | 11 | # Install dependencies without running scripts to avoid prepare script before build 12 | RUN npm install --ignore-scripts 13 | 14 | # Copy source files 15 | COPY src/ src/ 16 | 17 | # Build the TypeScript files 18 | RUN npm run build 19 | 20 | # Production image 21 | FROM node:20-alpine AS production 22 | 23 | # Set working directory 24 | WORKDIR /app 25 | 26 | # Copy built files from builder 27 | COPY --from=builder /app/build/ /app/build/ 28 | 29 | # Copy package.json and package-lock.json for production installation 30 | COPY package.json package-lock.json ./ 31 | 32 | # Install only production dependencies 33 | RUN npm ci --omit=dev 34 | 35 | # Set environment variables 36 | ENV NODE_ENV=production 37 | 38 | # Command to run the application 39 | ENTRYPOINT ["node", "build/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Angel Milla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // types.ts 2 | 3 | export interface NYTimesApiResponse { 4 | status: string; 5 | copyright: string; 6 | response: { 7 | docs: Article[]; 8 | meta: { 9 | hits: number; 10 | offset: number; 11 | time: number; 12 | }; 13 | }; 14 | } 15 | 16 | export interface Article { 17 | abstract: string; 18 | web_url: string; 19 | snippet: string; 20 | lead_paragraph: string; 21 | print_section?: string; 22 | print_page?: string; 23 | source: string; 24 | multimedia: any[]; 25 | headline: { 26 | main: string; 27 | kicker?: string; 28 | content_kicker?: string; 29 | print_headline?: string; 30 | name?: string; 31 | seo?: string; 32 | sub?: string; 33 | }; 34 | keywords: Array<{ 35 | name: string; 36 | value: string; 37 | rank: number; 38 | major: string; 39 | }>; 40 | pub_date: string; 41 | document_type: string; 42 | news_desk: string; 43 | section_name: string; 44 | byline: { 45 | original?: string; 46 | person?: Array<{ 47 | firstname: string; 48 | middlename?: string; 49 | lastname: string; 50 | qualifier?: string; 51 | title?: string; 52 | role?: string; 53 | organization?: string; 54 | rank: number; 55 | }>; 56 | organization?: string; 57 | }; 58 | type_of_material: string; 59 | _id: string; 60 | word_count: number; 61 | uri: string; 62 | } 63 | 64 | export interface SearchArticlesArgs { 65 | keyword: string; 66 | } 67 | 68 | // Type guard for search arguments 69 | export function isValidSearchArticlesArgs(args: any): args is SearchArticlesArgs { 70 | return ( 71 | typeof args === "object" && 72 | args !== null && 73 | "keyword" in args && 74 | typeof args.keyword === "string" 75 | ); 76 | } 77 | 78 | export interface ArticleSearchResult { 79 | title: string; 80 | abstract: string; 81 | url: string; 82 | publishedDate: string; 83 | author: string; 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NYTimes Article Search MCP Server 2 | 3 | [![smithery badge](https://smithery.ai/badge/nyt)](https://smithery.ai/server/nyt) 4 | 5 | This is a TypeScript-based MCP server that allows searching for New York Times articles from the last 30 days based on a keyword. It demonstrates core MCP concepts by providing: 6 | 7 | - Tools for searching articles 8 | - Integration with the New York Times API 9 | 10 | ![NYTimes Article Search](img/example.png) 11 | 12 | NYTimes Article Search Server MCP server 13 | 14 | ## Features 15 | 16 | ### Tools 17 | - `search_articles` - Search NYTimes articles from the last 30 days based on a keyword 18 | - Takes `keyword` as a required parameter 19 | - Returns a list of articles with title, abstract, URL, published date, and author 20 | 21 | ## Development 22 | 23 | Install dependencies: 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | Build the server: 29 | ```bash 30 | npm run build 31 | ``` 32 | 33 | For development with auto-rebuild: 34 | ```bash 35 | npm run watch 36 | ``` 37 | 38 | ### Debugging 39 | 40 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: 41 | 42 | ```bash 43 | npm run inspector 44 | ``` 45 | 46 | The Inspector will provide a URL to access debugging tools in your browser. 47 | 48 | ## Installation 49 | 50 | ### Installing via Smithery 51 | 52 | To install NYTimes Article Search for Claude Desktop automatically via [Smithery](https://smithery.ai/server/nyt): 53 | 54 | ```bash 55 | npx -y @smithery/cli install nyt --client claude 56 | ``` 57 | 58 | ### Manual Installation 59 | To use with Claude Desktop, add the server config: 60 | 61 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 62 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 63 | 64 | ```json 65 | { 66 | "mcpServers": { 67 | "nyt": { 68 | "command": "node", 69 | "args": ["path/to/your/build/index.js"], 70 | "env": { 71 | "NYTIMES_API_KEY": "your_api_key_here" 72 | } 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | ## Environment Variables 79 | 80 | Create a `.env` file in the root of your project and add your New York Times API key: 81 | 82 | ``` 83 | NYTIMES_API_KEY=your_api_key_here 84 | ``` 85 | 86 | ## Running the Server 87 | 88 | After building the project, you can run the server with: 89 | 90 | ```bash 91 | node build/index.js 92 | ``` 93 | 94 | The server will start and listen for MCP requests over stdio. 95 | 96 | ## License 97 | 98 | This project is licensed under the MIT License. 99 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { 5 | ListResourcesRequestSchema, 6 | ReadResourceRequestSchema, 7 | ListToolsRequestSchema, 8 | CallToolRequestSchema, 9 | ErrorCode, 10 | McpError 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import axios from "axios"; 13 | import dotenv from "dotenv"; 14 | import { 15 | NYTimesApiResponse, 16 | ArticleSearchResult, 17 | isValidSearchArticlesArgs 18 | } from "./types.js"; 19 | 20 | dotenv.config(); 21 | 22 | const API_KEY = process.env.NYTIMES_API_KEY; 23 | if (!API_KEY) { 24 | throw new Error("NYTIMES_API_KEY environment variable is required"); 25 | } 26 | 27 | const API_CONFIG = { 28 | BASE_URL: 'https://api.nytimes.com/svc/search/v2', 29 | ENDPOINT: 'articlesearch.json' 30 | } as const; 31 | 32 | class NYTimesServer { 33 | private server: Server; 34 | private axiosInstance; 35 | 36 | constructor() { 37 | this.server = new Server({ 38 | name: "nytimes-article-search-server", 39 | version: "0.1.0" 40 | }, { 41 | capabilities: { 42 | resources: {}, 43 | tools: {} 44 | } 45 | }); 46 | 47 | this.axiosInstance = axios.create({ 48 | baseURL: API_CONFIG.BASE_URL, 49 | params: { 50 | 'api-key': API_KEY 51 | } 52 | }); 53 | 54 | this.setupHandlers(); 55 | this.setupErrorHandling(); 56 | } 57 | 58 | private setupErrorHandling(): void { 59 | this.server.onerror = (error) => { 60 | console.error("[MCP Error]", error); 61 | }; 62 | 63 | process.on('SIGINT', async () => { 64 | await this.server.close(); 65 | process.exit(0); 66 | }); 67 | } 68 | 69 | private setupHandlers(): void { 70 | this.setupResourceHandlers(); 71 | this.setupToolHandlers(); 72 | } 73 | 74 | private setupResourceHandlers(): void { 75 | this.server.setRequestHandler( 76 | ListResourcesRequestSchema, 77 | async () => ({ 78 | resources: [] // No static resources for this server 79 | }) 80 | ); 81 | 82 | this.server.setRequestHandler( 83 | ReadResourceRequestSchema, 84 | async (request) => { 85 | throw new McpError( 86 | ErrorCode.InvalidRequest, 87 | `Unknown resource: ${request.params.uri}` 88 | ); 89 | } 90 | ); 91 | } 92 | 93 | private setupToolHandlers(): void { 94 | this.server.setRequestHandler( 95 | ListToolsRequestSchema, 96 | async () => ({ 97 | tools: [{ 98 | name: "search_articles", 99 | description: "Search NYTimes articles from the last 30 days based on a keyword", 100 | inputSchema: { 101 | type: "object", 102 | properties: { 103 | keyword: { 104 | type: "string", 105 | description: "Keyword to search for in articles" 106 | } 107 | }, 108 | required: ["keyword"] 109 | } 110 | }] 111 | }) 112 | ); 113 | 114 | this.server.setRequestHandler( 115 | CallToolRequestSchema, 116 | async (request) => { 117 | if (request.params.name !== "search_articles") { 118 | throw new McpError( 119 | ErrorCode.MethodNotFound, 120 | `Unknown tool: ${request.params.name}` 121 | ); 122 | } 123 | 124 | if (!isValidSearchArticlesArgs(request.params.arguments)) { 125 | throw new McpError( 126 | ErrorCode.InvalidParams, 127 | "Invalid search arguments" 128 | ); 129 | } 130 | 131 | const keyword = request.params.arguments.keyword; 132 | 133 | try { 134 | const response = await this.axiosInstance.get(API_CONFIG.ENDPOINT, { 135 | params: { 136 | q: keyword, 137 | sort: 'newest', 138 | 'begin_date': this.getDateString(30), // 30 days ago 139 | 'end_date': this.getDateString(0) // today 140 | } 141 | }); 142 | 143 | const articles: ArticleSearchResult[] = response.data.response.docs.map(article => ({ 144 | title: article.headline.main, 145 | abstract: article.abstract, 146 | url: article.web_url, 147 | publishedDate: article.pub_date, 148 | author: article.byline.original || 'Unknown' 149 | })); 150 | 151 | return { 152 | content: [{ 153 | type: "text", 154 | text: JSON.stringify(articles, null, 2) 155 | }] 156 | }; 157 | } catch (error) { 158 | if (axios.isAxiosError(error)) { 159 | return { 160 | content: [{ 161 | type: "text", 162 | text: `NYTimes API error: ${error.response?.data.message ?? error.message}` 163 | }], 164 | isError: true, 165 | } 166 | } 167 | throw error; 168 | } 169 | } 170 | ); 171 | } 172 | 173 | private getDateString(daysAgo: number): string { 174 | const date = new Date(); 175 | date.setDate(date.getDate() - daysAgo); 176 | return date.toISOString().split('T')[0].replace(/-/g, ''); 177 | } 178 | 179 | async run(): Promise { 180 | const transport = new StdioServerTransport(); 181 | await this.server.connect(transport); 182 | 183 | console.error("NYTimes MCP server running on stdio"); 184 | } 185 | } 186 | 187 | const server = new NYTimesServer(); 188 | server.run().catch(console.error); 189 | --------------------------------------------------------------------------------