@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.
- package/README.md +217 -0
- package/package.json +46 -0
- package/server.mjs +179 -0
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# NewsIntel MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://modelcontextprotocol.io)
|
|
4
|
+
[](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
|
+
}
|