@stabem/newsintel-mcp 0.1.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.
Files changed (3) hide show
  1. package/README.md +217 -0
  2. package/package.json +46 -0
  3. package/server.mjs +179 -0
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # NewsIntel MCP Server
2
+
3
+ [![MCP](https://img.shields.io/badge/MCP-compatible-6f42c1)](https://modelcontextprotocol.io)
4
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-339933)](https://nodejs.org)
5
+
6
+ MCP server para integrações dinâmicas com NewsIntel usando **token do próprio usuário** em cada chamada.
7
+
8
+ > Objetivo: permitir que qualquer cliente de IA (secretaria, assistentes, apps de terceiros) use o mesmo contrato MCP, sem hardcode por usuário.
9
+
10
+ ---
11
+
12
+ ## Sumário
13
+
14
+ - [Arquitetura](#arquitetura)
15
+ - [Pré-requisitos](#pré-requisitos)
16
+ - [Instalação](#instalação)
17
+ - [Executar](#executar)
18
+ - [Configuração](#configuração)
19
+ - [Ferramentas MCP](#ferramentas-mcp)
20
+ - [Exemplos de uso](#exemplos-de-uso)
21
+ - [Modelo de erros](#modelo-de-erros)
22
+ - [Segurança](#segurança)
23
+ - [Desenvolvimento](#desenvolvimento)
24
+ - [FAQ](#faq)
25
+
26
+ ---
27
+
28
+ ## Arquitetura
29
+
30
+ O servidor roda via `stdio` e atua como **proxy fino** para a API NewsIntel:
31
+
32
+ 1. Cliente MCP chama uma tool
33
+ 2. Tool recebe `apiToken` do usuário final
34
+ 3. Servidor chama endpoint NewsIntel correspondente
35
+ 4. Retorna JSON estruturado para o cliente MCP
36
+
37
+ Sem estado local por usuário, sem sessão persistente no MCP.
38
+
39
+ ---
40
+
41
+ ## Pré-requisitos
42
+
43
+ - Node.js `>= 22`
44
+ - Acesso HTTP ao endpoint da NewsIntel API
45
+ - Token de usuário NewsIntel (`ni_live_...`)
46
+
47
+ ---
48
+
49
+ ## Instalação
50
+
51
+ ```bash
52
+ cd integrations/newsintel-mcp
53
+ npm install
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Executar
59
+
60
+ ```bash
61
+ npm start
62
+ ```
63
+
64
+ Isso inicia o servidor MCP em `stdio` (modo esperado por clientes MCP).
65
+
66
+ ---
67
+
68
+ ## Configuração
69
+
70
+ Variáveis de ambiente suportadas:
71
+
72
+ | Variável | Obrigatória | Default | Descrição |
73
+ |---|---:|---|---|
74
+ | `NEWSINTEL_API_BASE` | não | `https://newsintelapi.com` | Base URL da API NewsIntel |
75
+
76
+ Exemplo:
77
+
78
+ ```bash
79
+ NEWSINTEL_API_BASE=https://newsintelapi.com npm start
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Ferramentas MCP
85
+
86
+ ### 1) `newsintel_sync_profile`
87
+ Sincroniza/personaliza o perfil do usuário.
88
+
89
+ **Input**
90
+ - `apiToken` (string, obrigatório)
91
+ - `mode` (`manual` | `x-browser` | `x-api`, opcional, default `manual`)
92
+ - `topics` (string[], opcional)
93
+ - `free_text` (string, opcional)
94
+ - `x_username` (string, opcional)
95
+
96
+ **Backend**
97
+ - `POST /v1/profile/sync`
98
+
99
+ ---
100
+
101
+ ### 2) `newsintel_get_profile`
102
+ Busca perfil/interesses atuais do usuário.
103
+
104
+ **Input**
105
+ - `apiToken` (string, obrigatório)
106
+
107
+ **Backend**
108
+ - `GET /v1/profile`
109
+
110
+ ---
111
+
112
+ ### 3) `newsintel_get_news_briefing`
113
+ Busca notícias personalizadas com filtros.
114
+
115
+ **Input**
116
+ - `apiToken` (string, obrigatório)
117
+ - `topics` (string[], opcional)
118
+ - `country` (string, opcional, ex: `BR`, `US`)
119
+ - `lang` (string[], opcional, ex: `['pt','en']`)
120
+ - `limit` (number, opcional, `5..50`, default `20`)
121
+
122
+ **Backend**
123
+ - `GET /v1/news/search`
124
+
125
+ ---
126
+
127
+ ## Exemplos de uso
128
+
129
+ ### Sync manual com tópicos explícitos
130
+
131
+ ```json
132
+ {
133
+ "apiToken": "ni_live_xxx",
134
+ "mode": "manual",
135
+ "topics": ["spfc", "crypto", "macro"]
136
+ }
137
+ ```
138
+
139
+ ### Buscar perfil
140
+
141
+ ```json
142
+ {
143
+ "apiToken": "ni_live_xxx"
144
+ }
145
+ ```
146
+
147
+ ### Buscar briefing BR/PT
148
+
149
+ ```json
150
+ {
151
+ "apiToken": "ni_live_xxx",
152
+ "topics": ["spfc", "mercados"],
153
+ "country": "BR",
154
+ "lang": ["pt"],
155
+ "limit": 20
156
+ }
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Modelo de erros
162
+
163
+ Erros de API são retornados como:
164
+
165
+ ```text
166
+ newsintel_api_error <status>: <payload-json-truncado>
167
+ ```
168
+
169
+ Exemplos comuns:
170
+ - `401/403`: token inválido ou sem permissão
171
+ - `429`: rate limit
172
+ - `5xx`: indisponibilidade temporária
173
+
174
+ Recomendado no cliente:
175
+ - retry com backoff para `429/5xx`
176
+ - refresh/reauth para `401/403`
177
+
178
+ ---
179
+
180
+ ## Segurança
181
+
182
+ - Trate `apiToken` como segredo.
183
+ - Não salve token em logs, analytics ou mensagens de erro públicas.
184
+ - Prefira injeção via secrets manager.
185
+ - Se houver suspeita de vazamento: rotacione o token imediatamente.
186
+
187
+ ---
188
+
189
+ ## Desenvolvimento
190
+
191
+ Estrutura:
192
+
193
+ ```text
194
+ integrations/newsintel-mcp/
195
+ ├── server.mjs
196
+ ├── package.json
197
+ └── README.md
198
+ ```
199
+
200
+ Checagem rápida de sintaxe:
201
+
202
+ ```bash
203
+ node --check server.mjs
204
+ ```
205
+
206
+ ---
207
+
208
+ ## FAQ
209
+
210
+ **Preciso de um MCP por usuário?**
211
+ - Não. O mesmo servidor atende múltiplos usuários via `apiToken` por chamada.
212
+
213
+ **Posso apontar para outra API base (staging)?**
214
+ - Sim, usando `NEWSINTEL_API_BASE`.
215
+
216
+ **Esse MCP guarda estado?**
217
+ - Não. É stateless; toda personalização vem da API NewsIntel.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@stabem/newsintel-mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "MCP server for NewsIntel — personalized news intelligence for AI agents",
6
+ "mcpName": "io.github.stabem/newsintel",
7
+ "main": "server.mjs",
8
+ "bin": {
9
+ "newsintel-mcp": "server.mjs"
10
+ },
11
+ "scripts": {
12
+ "start": "node server.mjs",
13
+ "test": "node --test server.test.mjs"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "newsintel",
19
+ "news",
20
+ "ai-agent",
21
+ "claude",
22
+ "llm",
23
+ "briefing"
24
+ ],
25
+ "author": "stabem",
26
+ "license": "ISC",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/stabem/newsintel-api.git",
30
+ "directory": "integrations/newsintel-mcp"
31
+ },
32
+ "homepage": "https://github.com/stabem/newsintel-api/tree/main/integrations/newsintel-mcp",
33
+ "bugs": {
34
+ "url": "https://github.com/stabem/newsintel-api/issues"
35
+ },
36
+ "files": [
37
+ "server.mjs",
38
+ "README.md"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.17.2"
45
+ }
46
+ }
package/server.mjs ADDED
@@ -0,0 +1,179 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+
9
+ const API_BASE = process.env.NEWSINTEL_API_BASE || 'https://newsintelapi.com';
10
+
11
+ async function apiRequest({ apiToken, method = 'GET', path, body }) {
12
+ if (!apiToken || typeof apiToken !== 'string') {
13
+ throw new Error('apiToken is required');
14
+ }
15
+
16
+ const res = await fetch(`${API_BASE}${path}`, {
17
+ method,
18
+ headers: {
19
+ Authorization: `Bearer ${apiToken}`,
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ body: body ? JSON.stringify(body) : undefined,
23
+ });
24
+
25
+ const text = await res.text();
26
+ let data;
27
+ try {
28
+ data = text ? JSON.parse(text) : {};
29
+ } catch {
30
+ data = { raw: text };
31
+ }
32
+
33
+ if (!res.ok) {
34
+ throw new Error(`newsintel_api_error ${res.status}: ${JSON.stringify(data).slice(0, 600)}`);
35
+ }
36
+
37
+ return data;
38
+ }
39
+
40
+ const server = new Server(
41
+ {
42
+ name: 'newsintel-mcp-server',
43
+ version: '0.1.0',
44
+ },
45
+ {
46
+ capabilities: {
47
+ tools: {},
48
+ },
49
+ },
50
+ );
51
+
52
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
53
+ return {
54
+ tools: [
55
+ {
56
+ name: 'newsintel_sync_profile',
57
+ description: 'Sync/personalize NewsIntel profile dynamically for a user token',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ apiToken: { type: 'string', description: 'User API token (ni_live_...)' },
62
+ mode: { type: 'string', enum: ['manual', 'x-browser', 'x-api'], default: 'manual' },
63
+ topics: {
64
+ type: 'array',
65
+ items: { type: 'string' },
66
+ description: 'Optional explicit interests/topics',
67
+ },
68
+ free_text: { type: 'string' },
69
+ x_username: { type: 'string' },
70
+ },
71
+ required: ['apiToken'],
72
+ },
73
+ },
74
+ {
75
+ name: 'newsintel_get_profile',
76
+ description: 'Fetch user profile/interests for a user token',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ apiToken: { type: 'string', description: 'User API token (ni_live_...)' },
81
+ },
82
+ required: ['apiToken'],
83
+ },
84
+ },
85
+ {
86
+ name: 'newsintel_get_news_briefing',
87
+ description: 'Fetch personalized news with optional topics/country/language filters',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ apiToken: { type: 'string', description: 'User API token (ni_live_...)' },
92
+ topics: { type: 'array', items: { type: 'string' } },
93
+ country: { type: 'string', description: 'Country code, e.g. BR or US' },
94
+ lang: {
95
+ type: 'array',
96
+ items: { type: 'string' },
97
+ description: 'Languages, e.g. ["pt","en"]',
98
+ },
99
+ limit: { type: 'number', minimum: 5, maximum: 50, default: 20 },
100
+ },
101
+ required: ['apiToken'],
102
+ },
103
+ },
104
+ ],
105
+ };
106
+ });
107
+
108
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
109
+ const { name, arguments: args = {} } = request.params;
110
+
111
+ if (name === 'newsintel_sync_profile') {
112
+ const body = {
113
+ mode: args.mode || 'manual',
114
+ topics: Array.isArray(args.topics) ? args.topics : undefined,
115
+ free_text: typeof args.free_text === 'string' ? args.free_text : undefined,
116
+ x_username: typeof args.x_username === 'string' ? args.x_username : undefined,
117
+ };
118
+
119
+ const data = await apiRequest({
120
+ apiToken: args.apiToken,
121
+ method: 'POST',
122
+ path: '/v1/profile/sync',
123
+ body,
124
+ });
125
+
126
+ return {
127
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
128
+ };
129
+ }
130
+
131
+ if (name === 'newsintel_get_profile') {
132
+ const data = await apiRequest({
133
+ apiToken: args.apiToken,
134
+ method: 'GET',
135
+ path: '/v1/profile',
136
+ });
137
+
138
+ return {
139
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
140
+ };
141
+ }
142
+
143
+ if (name === 'newsintel_get_news_briefing') {
144
+ const params = new URLSearchParams();
145
+ params.set('limit', String(Math.max(5, Math.min(50, Number(args.limit || 20)))));
146
+
147
+ if (Array.isArray(args.topics) && args.topics.length > 0) {
148
+ params.set('topics', args.topics.join(','));
149
+ }
150
+
151
+ if (typeof args.country === 'string' && args.country.trim()) {
152
+ params.set('country', args.country.trim().toUpperCase());
153
+ }
154
+
155
+ if (Array.isArray(args.lang) && args.lang.length > 0) {
156
+ params.set('lang', args.lang.join(','));
157
+ }
158
+
159
+ const data = await apiRequest({
160
+ apiToken: args.apiToken,
161
+ method: 'GET',
162
+ path: `/v1/news/search?${params.toString()}`,
163
+ });
164
+
165
+ return {
166
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
167
+ };
168
+ }
169
+
170
+ throw new Error(`Unknown tool: ${name}`);
171
+ });
172
+
173
+ export { server, apiRequest };
174
+
175
+ const isMain = process.argv[1] === fileURLToPath(import.meta.url);
176
+ if (isMain) {
177
+ const transport = new StdioServerTransport();
178
+ await server.connect(transport);
179
+ }