├── .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 | [](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 | 
11 |
12 |
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 |
--------------------------------------------------------------------------------