afpnews-mcp-server 2.1.0 → 3.0.0-next.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 (38) hide show
  1. package/.env.example +4 -1
  2. package/CHANGELOG.md +114 -0
  3. package/CLAUDE.md +10 -8
  4. package/README.md +39 -3
  5. package/bun.lock +137 -158
  6. package/package.json +15 -10
  7. package/src/__tests__/fixtures.ts +53 -3
  8. package/src/__tests__/format-media.test.ts +40 -34
  9. package/src/__tests__/format.test.ts +78 -25
  10. package/src/__tests__/get-media-handler.test.ts +49 -22
  11. package/src/__tests__/get-media.test.ts +33 -0
  12. package/src/__tests__/http-server.test.ts +50 -0
  13. package/src/__tests__/oauth-flow.test.ts +118 -0
  14. package/src/__tests__/search-media-handler.test.ts +143 -0
  15. package/src/__tests__/server.test.ts +61 -16
  16. package/src/__tests__/stdio-server.test.ts +38 -0
  17. package/src/__tests__/tokens.test.ts +87 -0
  18. package/src/__tests__/tool-defaults.test.ts +10 -7
  19. package/src/http/login-page.ts +16 -11
  20. package/src/http/server.ts +257 -220
  21. package/src/http/tokens.ts +69 -9
  22. package/src/http/worker.ts +31 -0
  23. package/src/mcp-server.ts +8 -4
  24. package/src/stdio/server.ts +1 -1
  25. package/src/tools/find-similar.ts +9 -3
  26. package/src/tools/get-article.ts +3 -2
  27. package/src/tools/get-media.ts +32 -90
  28. package/src/tools/list-facets.ts +30 -8
  29. package/src/tools/search-articles.ts +7 -5
  30. package/src/tools/search-media.ts +66 -10
  31. package/src/tools/shared.ts +5 -20
  32. package/src/utils/embed-media.ts +65 -0
  33. package/src/utils/format-media.ts +32 -31
  34. package/src/utils/format.ts +76 -47
  35. package/src/utils/types.ts +3 -20
  36. package/tsconfig.json +3 -1
  37. package/wrangler.toml +28 -0
  38. package/skills-lock.json +0 -10
package/.env.example CHANGED
@@ -9,4 +9,7 @@ JWT_SECRET= # générer avec: openssl rand -base64 32
9
9
  MCP_SERVER_URL=https://news-mcp.example.com
10
10
  PORT=3000 # optionnel, défaut: 3000
11
11
  MCP_TRANSPORT=http # stdio ou http
12
- MCP_SESSION_TTL=3600000 # optionnel, ms, défaut: 1h
12
+
13
+ # Callback OAuth des clients IA distants (hors localhost, toujours autorisé).
14
+ # Liste unique, séparée par des virgules — voir README.md pour les valeurs connues.
15
+ MCP_ALLOWED_REDIRECT_URIS=https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
package/CHANGELOG.md ADDED
@@ -0,0 +1,114 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format loosely follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [3.0.0] — 2026-08-25
8
+
9
+ ### Breaking
10
+
11
+ - **HTTP transport is now stateless.** The previous session-map model
12
+ (`mcp-session-id`, `MCP_SESSION_TTL`) is gone — every request now gets a
13
+ fresh `McpServer` built straight from its own bearer token, so any number
14
+ of instances can sit behind a plain load balancer with no sticky sessions.
15
+ Deployments that relied on `MCP_SESSION_TTL` or session affinity need to
16
+ drop that configuration.
17
+ - Migrated from `@modelcontextprotocol/sdk` (v1) to the split v2 packages
18
+ (`@modelcontextprotocol/server`, `@modelcontextprotocol/hono`), targeting
19
+ the 2026-07-28 MCP spec.
20
+ - HTTP framework: **Elysia → Hono**. Framework-specific behavior (error
21
+ response shapes in particular) changed accordingly.
22
+ - `/oauth/token` now only accepts `application/x-www-form-urlencoded`
23
+ request bodies (RFC 6749 §4.1.3), not JSON.
24
+ - Access tokens are now bound to this server's own `/mcp` resource via an
25
+ `aud` claim (RFC 8707); tokens minted by a different deployment are no
26
+ longer accepted even if it shares the same `JWT_SECRET`.
27
+ - 401 responses on `/mcp` now carry a spec-compliant `WWW-Authenticate`
28
+ header pointing at the protected-resource metadata document, replacing
29
+ the previous ad hoc header/body shape.
30
+
31
+ ### Added
32
+
33
+ - Deployable as a **Cloudflare Worker** (`src/http/worker.ts`, `wrangler.toml`)
34
+ with no KV/Durable Objects/other bindings — the OAuth authorization code
35
+ and bearer tokens are all self-contained.
36
+ - `src/http/server.ts` split into `resolveHttpConfig()` / `createHttpApp()` /
37
+ `startHttpServer()`, so the Hono app itself has no Bun or Node dependency.
38
+
39
+ ### Changed
40
+
41
+ - The OAuth authorization code is now a self-contained token (same JWE
42
+ pattern as the access/refresh tokens) instead of server-side state.
43
+ - `node:crypto` dropped in favor of Web Crypto (`crypto.subtle`) for HKDF
44
+ key derivation and the PKCE challenge check — portable across Bun,
45
+ browsers, and Workers.
46
+ - Rate limiting (`elysia-rate-limit`) removed; left to the hosting platform
47
+ (e.g. Cloudflare's own Rate Limiting) rather than a hand-rolled,
48
+ per-isolate in-process limiter.
49
+ - `/.well-known/oauth-authorization-server` and
50
+ `/.well-known/oauth-protected-resource/mcp` are now served via the SDK's
51
+ own metadata helpers instead of hand-written JSON.
52
+
53
+ ### Fixed
54
+
55
+ - The JWE's registered `exp` claim was silently overwritten by
56
+ `setExpirationTime()`, so decrypted tokens never carried the real AFP
57
+ token expiry — moved to a dedicated `texp` claim.
58
+ - A `globalThis.fetch` mock in `get-media-handler.test.ts` was leaking into
59
+ whichever test file ran next (test-isolation bug).
60
+
61
+ ## [2.2.0] — 2026-08-01
62
+
63
+ - Snapshot prior to the SDK v2 / Hono / Cloudflare Workers work above.
64
+
65
+ ## [2.0.2] — 2026-06-09
66
+
67
+ - Fix: `lang` facet examples corrected to `langs` in docs and tests.
68
+ - Improved typing, text truncation, and test infrastructure.
69
+
70
+ ## [2.0.0] — 2026-03-17
71
+
72
+ - Migrated package manager from pnpm to **Bun**; test runner from Vitest to
73
+ `bun test`.
74
+ - HTTP transport rewritten on **Elysia** (replacing Express), split into
75
+ `src/http/` submodules; stdio transport extracted to `src/stdio/server.ts`.
76
+ - OAuth2/OIDC hardening: AFP refresh token stored in a JWE (no credentials
77
+ persisted server-side), strict `redirect_uri` allowlist, and a round of
78
+ security-audit fixes.
79
+ - Added `afp_search_media` / `afp_get_media` tools, with base64 image embed
80
+ for vision models and dedicated media formatting (`format-media.ts`).
81
+ - `product` facet replaced with `class` throughout; added `event` field to
82
+ the AFP document model.
83
+ - `src/mcp-server.ts` renamed from `server.ts` to avoid a naming collision
84
+ with `http/server.ts`.
85
+
86
+ ## [1.3.10] — 2026-02-22
87
+
88
+ - `baseUrl` support (`APICORE_BASE_URL`) added and covered by tests.
89
+
90
+ ## [1.3.7] — 2026-02-20
91
+
92
+ - `afpshortid` field added; `afp_list_facets` fixes.
93
+
94
+ ## [1.3.4] — 2026-02-17
95
+
96
+ - Tool definitions' JSON Schemas generated via Zod v4's built-in `toJSONSchema`.
97
+
98
+ ## [1.3.2] and earlier
99
+
100
+ - Initial tool set (search, get article, similar articles, facets),
101
+ prompts (daily briefing, comprehensive analysis, factcheck, country news),
102
+ and the AFP Stories topics resource.
103
+ - HTTP transport added alongside stdio; auth token caching.
104
+ - Package renamed to `afpnews-mcp-server`; pre-converted JSON Schemas
105
+ exported for cross-instance compatibility.
106
+ - A Notification Center implementation was added and then reverted (the
107
+ underlying AFP API endpoint wasn't functional).
108
+
109
+ [3.0.0]: https://github.com/julesbonnard/afpnews-mcp-server/compare/v2.0.2...v3.0.0
110
+ [2.0.2]: https://github.com/julesbonnard/afpnews-mcp-server/compare/v2.0.0...v2.0.2
111
+ [2.0.0]: https://github.com/julesbonnard/afpnews-mcp-server/compare/v1.3.10...v2.0.0
112
+ [1.3.10]: https://github.com/julesbonnard/afpnews-mcp-server/compare/v1.3.7...v1.3.10
113
+ [1.3.7]: https://github.com/julesbonnard/afpnews-mcp-server/compare/v1.3.4...v1.3.7
114
+ [1.3.4]: https://github.com/julesbonnard/afpnews-mcp-server/compare/v1.3.2...v1.3.4
package/CLAUDE.md CHANGED
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project Overview
6
6
 
7
- MCP (Model Context Protocol) server that exposes AFP news tools for LLM-based editors (Continue, Claude Code, etc.). Supports stdio and HTTP (Streamable HTTP) transports.
7
+ MCP (Model Context Protocol) server that exposes AFP news tools for LLM-based editors (Continue, Claude Code, etc.). Supports stdio and HTTP (stateless Streamable HTTP) transports, targeting the 2026-07-28 MCP spec via the `@modelcontextprotocol/server` v2 SDK.
8
8
 
9
9
  ## Commands
10
10
 
@@ -42,8 +42,9 @@ src/
42
42
  │ ├── index.ts # registerResources() MCP glue
43
43
  │ └── topics.ts # TOPICS inline + resource handler
44
44
  ├── http/
45
- │ ├── server.ts # Elysia HTTP server + OAuth2 PKCE auth
46
- │ ├── tokens.ts # JWT helpers (encrypt/decrypt AFP tokens)
45
+ │ ├── server.ts # resolveHttpConfig() + createHttpApp() (Hono, platform-agnostic) + startHttpServer() (Bun)
46
+ │ ├── worker.ts # Cloudflare Worker entry point — same createHttpApp(), config from the `env` binding
47
+ │ ├── tokens.ts # JWT helpers (encrypt/decrypt AFP tokens, self-contained auth code)
47
48
  │ └── login-page.ts # OAuth login page + redirect URI helpers
48
49
  ├── stdio/
49
50
  │ └── server.ts # stdio transport entry
@@ -54,9 +55,9 @@ src/
54
55
  ```
55
56
 
56
57
  1. Creates an `ApiCore` client from `afpnews-api` using `APICORE_API_KEY`
57
- 2. Registers MCP tools via `@modelcontextprotocol/sdk`: `afp_search_articles`, `afp_get_article`, `afp_find_similar`, `afp_list_facets`, `afp_search_media`, `afp_get_media`
58
+ 2. Registers MCP tools via `@modelcontextprotocol/server`: `afp_search_articles`, `afp_get_article`, `afp_find_similar`, `afp_list_facets`, `afp_search_media`, `afp_get_media`
58
59
  3. Authenticates with username/password on first call, then reuses or refreshes the token for subsequent queries
59
- 4. Supports two transports: stdio (default) and HTTP (`MCP_TRANSPORT=http`, uses Elysia + Streamable HTTP with OAuth2 PKCE per-session)
60
+ 4. Supports two transports: stdio (default) and HTTP (`MCP_TRANSPORT=http`, uses Hono + `createMcpHandler()` — a fresh `McpServer` per request, no session store or other server-side state, OAuth2 PKCE per-request bearer token). HTTP has two entry points sharing the same `createHttpApp()`: `startHttpServer()` (Bun, `Bun.serve`) and `worker.ts` (Cloudflare Worker, `export default { fetch }`) — see "Cloudflare Workers" in README.md for deployment.
60
61
 
61
62
  ### Definitions-First Pattern
62
63
 
@@ -87,9 +88,10 @@ Required:
87
88
  - `MCP_TRANSPORT=http`
88
89
 
89
90
  Optional:
90
- - `PORT` — HTTP server port (default: 3000)
91
- - `MCP_SESSION_TTL` — Session duration in milliseconds (default: 3600000 = 1h)
92
- - `MCP_ALLOWED_REDIRECT_URIS` — Comma-separated list of allowed OAuth redirect URIs
91
+ - `PORT` — HTTP server port (default: 3000; ignored on Cloudflare Workers, which has no port to bind)
92
+ - `MCP_ALLOWED_REDIRECT_URIS` — Comma-separated list of allowed OAuth redirect URIs. The single place to allowlist AI clients with a fixed callback URL (Claude, ChatGPT, Mistral...) — `localhost`/`127.0.0.1` is always allowed separately, no entry needed for local clients. See README.md for known values.
93
+
94
+ On Workers, these are read from the `env` binding (`wrangler.toml` `[vars]` + `wrangler secret put`) instead of `process.env`, which doesn't exist there — see `src/http/worker.ts` and README.md.
93
95
 
94
96
  ## Key Details
95
97
 
package/README.md CHANGED
@@ -50,7 +50,10 @@ For local MCP clients like Claude Code or Claude Desktop:
50
50
 
51
51
  ### HTTP transport
52
52
 
53
- For remote or multi-user deployments. Users authenticate via OAuth2 PKCE using their AFP credentials.
53
+ For remote or multi-user deployments. Stateless: each request is served by a fresh MCP server
54
+ instance built from that request's own bearer token, so any number of instances can sit behind
55
+ a plain round-robin load balancer with no sticky sessions or shared store. Users authenticate via
56
+ OAuth2 PKCE using their AFP credentials.
54
57
 
55
58
  Required environment variables for HTTP mode:
56
59
 
@@ -64,8 +67,17 @@ PORT=3000
64
67
  ```
65
68
 
66
69
  Optional:
67
- - `MCP_SESSION_TTL` — session duration in milliseconds (default: 3600000 = 1h)
68
- - `MCP_ALLOWED_REDIRECT_URIS` — comma-separated list of allowed OAuth redirect URIs
70
+ - `MCP_ALLOWED_REDIRECT_URIS` — comma-separated list of allowed OAuth redirect URIs.
71
+ `localhost`/`127.0.0.1` (any port) is always allowed regardless of this list —
72
+ it covers any client that runs a local OAuth callback server (Claude Code,
73
+ MCP Inspector, most IDEs). This var is the single place to allowlist clients
74
+ with a fixed, non-localhost callback URL. Known ones:
75
+ - Claude: `https://claude.ai/api/mcp/auth_callback`, `https://claude.com/api/mcp/auth_callback`
76
+ - ChatGPT: `https://chatgpt.com/connector_platform_oauth_redirect`, `https://chatgpt.com/oauth/callback`, `https://chat.openai.com/oauth/callback`
77
+ - Mistral (Le Chat): `https://callback.mistral.ai/v1/integrations_auth/oauth2_callback`
78
+
79
+ Add others as you connect new clients — the exact `redirect_uri` shows up in
80
+ the failed `/oauth/authorize` request when a client isn't allowlisted yet.
69
81
 
70
82
  ```bash
71
83
  bun src/index.ts
@@ -87,6 +99,30 @@ docker run \
87
99
  afpnews-mcp
88
100
  ```
89
101
 
102
+ ### Cloudflare Workers
103
+
104
+ The HTTP transport is built as a plain [Hono](https://hono.dev) app with no server-side state
105
+ (the OAuth authorization code and the bearer tokens are all self-contained), so it also runs as a
106
+ Cloudflare Worker via `src/http/worker.ts` — no KV, Durable Objects, or other bindings needed.
107
+
108
+ ```bash
109
+ bunx wrangler login
110
+ bunx wrangler secret put APICORE_API_KEY
111
+ bunx wrangler secret put JWT_SECRET
112
+ ```
113
+
114
+ Edit `wrangler.toml`'s `[vars]` (`APICORE_BASE_URL`, `MCP_SERVER_URL` — the latter must match your
115
+ actual `*.workers.dev` subdomain or custom domain), then:
116
+
117
+ ```bash
118
+ bun run dev:worker # local dev server via workerd (bunx wrangler dev)
119
+ bun run deploy:worker # bunx wrangler deploy
120
+ ```
121
+
122
+ Rate limiting isn't implemented in-process (it would be per-isolate and largely pointless on the
123
+ edge) — use [Cloudflare's own Rate Limiting](https://developers.cloudflare.com/waf/rate-limiting-rules/)
124
+ in front of the Worker instead.
125
+
90
126
  ### As a library (without MCP server dependency)
91
127
 
92
128
  You can import pure definitions (tools, prompts, resources) and wire them into your own runtime: