@alfe.ai/news-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { resolveConfig } from "@alfe.ai/config";
5
+ import { AgentApiClient } from "@alfe.ai/agent-api-client";
6
+ import { z } from "zod";
7
+ //#region src/tools.ts
8
+ /**
9
+ * MCP tool registration for the News MCP server.
10
+ *
11
+ * Tools:
12
+ * news_search → client.newsSearch(...) (POST /agent/news/search)
13
+ * top_headlines → client.newsHeadlines(...) (POST /agent/news/headlines)
14
+ *
15
+ * Both take a strict zod input schema. `provider` is an ENUM
16
+ * (`apitube | newsdata`) defaulting to `apitube` — never free-text, because a
17
+ * typo'd provider server-side is an unpriceable product. All provider keys and
18
+ * billing live server-side in `services/news`; this server only forwards a
19
+ * typed body via `@alfe.ai/agent-api-client` and normalizes errors.
20
+ *
21
+ * Provider / billing / HTTP errors are surfaced as an `isError` MCP result
22
+ * carrying the error text — never a fake success.
23
+ */
24
+ const providerSchema = z.enum(["apitube", "newsdata"]).default("apitube").describe("News provider to query. APITube (default) covers 500K+ sources with sentiment.");
25
+ function ok(data) {
26
+ return { content: [{
27
+ type: "text",
28
+ text: JSON.stringify(data)
29
+ }] };
30
+ }
31
+ function fail(err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ return {
34
+ content: [{
35
+ type: "text",
36
+ text: JSON.stringify({ error: message })
37
+ }],
38
+ isError: true
39
+ };
40
+ }
41
+ function registerTools(server, client) {
42
+ const register = server.registerTool.bind(server);
43
+ register("news_search", {
44
+ description: "Search recent news across a broad provider (APITube / NewsData). Returns normalized articles with title, url, source, publishedAt, snippet, and (APITube) sentiment.",
45
+ inputSchema: {
46
+ query: z.string().min(1).describe("Search query (keywords, entities, topics)."),
47
+ provider: providerSchema,
48
+ source: z.string().optional().describe("Restrict to a single source/domain (e.g. reuters.com)."),
49
+ from: z.string().optional().describe("Earliest publish date, ISO-8601 (e.g. 2026-07-01)."),
50
+ to: z.string().optional().describe("Latest publish date, ISO-8601."),
51
+ language: z.string().optional().describe("ISO-639-1 language code (e.g. en)."),
52
+ category: z.string().optional().describe("News category (e.g. business, technology)."),
53
+ limit: z.number().int().positive().max(100).optional().describe("Max articles to return.")
54
+ }
55
+ }, async (args) => {
56
+ try {
57
+ return ok(await client.newsSearch({
58
+ query: args.query,
59
+ provider: args.provider,
60
+ source: args.source,
61
+ from: args.from,
62
+ to: args.to,
63
+ language: args.language,
64
+ category: args.category,
65
+ limit: args.limit
66
+ }));
67
+ } catch (err) {
68
+ return fail(err);
69
+ }
70
+ });
71
+ register("top_headlines", {
72
+ description: "Fetch current top headlines for a provider, optionally filtered by category, source, or language.",
73
+ inputSchema: {
74
+ provider: providerSchema,
75
+ category: z.string().optional().describe("News category (e.g. business, technology)."),
76
+ source: z.string().optional().describe("Restrict to a single source/domain."),
77
+ language: z.string().optional().describe("ISO-639-1 language code (e.g. en)."),
78
+ limit: z.number().int().positive().max(100).optional().describe("Max headlines to return.")
79
+ }
80
+ }, async (args) => {
81
+ try {
82
+ return ok(await client.newsHeadlines({
83
+ provider: args.provider,
84
+ category: args.category,
85
+ source: args.source,
86
+ language: args.language,
87
+ limit: args.limit
88
+ }));
89
+ } catch (err) {
90
+ return fail(err);
91
+ }
92
+ });
93
+ }
94
+ //#endregion
95
+ //#region src/server.ts
96
+ /**
97
+ * News MCP Server
98
+ *
99
+ * Standalone stdio MCP server for broad news search (APITube / NewsData behind
100
+ * a `provider` arg). Runtime-agnostic — spawned via `npx -y @alfe.ai/news-mcp`
101
+ * from an integration manifest, speaks MCP over stdio, so it runs under any
102
+ * runtime that can spawn `npx`.
103
+ *
104
+ * Architecture:
105
+ * Agent runtime ←(stdio/MCP)→ this server ←(HTTPS)→ services/news (metered)
106
+ *
107
+ * No provider credentials live on the box. At startup we self-fetch the agent's
108
+ * `{ apiKey, apiUrl }` via `resolveConfig()` and call the metered `services/news`
109
+ * Lambda through `AgentApiClient`. Provider keys + billing are server-side; the
110
+ * Lambda is the metering chokepoint.
111
+ */
112
+ function log(msg) {
113
+ process.stderr.write(`[news-mcp] ${msg}\n`);
114
+ }
115
+ async function main() {
116
+ const { apiKey, apiUrl } = resolveConfig();
117
+ const client = new AgentApiClient({
118
+ apiKey,
119
+ apiUrl
120
+ });
121
+ const server = new McpServer({
122
+ name: "news-mcp-server",
123
+ version: "0.1.0"
124
+ });
125
+ registerTools(server, client);
126
+ const shutdown = () => {
127
+ process.exit(0);
128
+ };
129
+ for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, shutdown);
130
+ const transport = new StdioServerTransport();
131
+ await server.connect(transport);
132
+ log("News MCP server running (news_search, top_headlines)");
133
+ }
134
+ main().catch((err) => {
135
+ log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
136
+ process.exit(1);
137
+ });
138
+ //#endregion
139
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@alfe.ai/news-mcp",
3
+ "version": "0.2.0",
4
+ "description": "News MCP server — broad news search (APITube / NewsData) with sentiment, metered server-side via Alfe",
5
+ "type": "module",
6
+ "main": "./dist/server.js",
7
+ "bin": {
8
+ "news-mcp-server": "./dist/server.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/server.d.ts",
13
+ "import": "./dist/server.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.29.0",
21
+ "zod": "^4.0.5",
22
+ "@alfe.ai/agent-api-client": "0.11.0",
23
+ "@alfe.ai/config": "0.3.0"
24
+ },
25
+ "license": "UNLICENSED",
26
+ "homepage": "https://alfe.ai",
27
+ "author": "Alfe (https://alfe.ai)",
28
+ "keywords": [
29
+ "alfe",
30
+ "ai-agents",
31
+ "agent",
32
+ "llm",
33
+ "mcp",
34
+ "model-context-protocol",
35
+ "news"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "dev": "tsdown --watch",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run",
42
+ "lint": "eslint ."
43
+ }
44
+ }