@markmnl/fmsg-mcp 0.1.4 → 0.2.1

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 (64) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +128 -26
  3. package/ROADMAP.md +52 -0
  4. package/SECURITY.md +78 -0
  5. package/dist/auth.d.ts +14 -6
  6. package/dist/auth.js +115 -49
  7. package/dist/client/client.d.ts +34 -24
  8. package/dist/client/client.js +152 -105
  9. package/dist/client/errors.d.ts +19 -0
  10. package/dist/client/errors.js +51 -0
  11. package/dist/client/index.d.ts +1 -0
  12. package/dist/client/stream.d.ts +12 -0
  13. package/dist/client/stream.js +83 -0
  14. package/dist/client/token-provider.d.ts +27 -0
  15. package/dist/client/token-provider.js +54 -0
  16. package/dist/client/types.d.ts +8 -4
  17. package/dist/client/url.d.ts +4 -0
  18. package/dist/client/url.js +35 -0
  19. package/dist/client/ws.d.ts +2 -1
  20. package/dist/client/ws.js +9 -3
  21. package/dist/config.d.ts +9 -3
  22. package/dist/config.js +21 -6
  23. package/dist/context.d.ts +6 -3
  24. package/dist/context.js +9 -2
  25. package/dist/download.d.ts +11 -0
  26. package/dist/download.js +34 -0
  27. package/dist/errors.d.ts +1 -1
  28. package/dist/errors.js +18 -26
  29. package/dist/http.d.ts +4 -3
  30. package/dist/http.js +246 -28
  31. package/dist/index.js +27 -11
  32. package/dist/instructions.js +12 -5
  33. package/dist/oauth/config.d.ts +11 -0
  34. package/dist/oauth/config.js +46 -0
  35. package/dist/oauth/errors.d.ts +18 -0
  36. package/dist/oauth/errors.js +40 -0
  37. package/dist/oauth/issuer.d.ts +31 -0
  38. package/dist/oauth/issuer.js +185 -0
  39. package/dist/oauth/provider.d.ts +23 -0
  40. package/dist/oauth/provider.js +95 -0
  41. package/dist/oauth/scopes.d.ts +6 -0
  42. package/dist/oauth/scopes.js +31 -0
  43. package/dist/render.d.ts +6 -1
  44. package/dist/render.js +27 -11
  45. package/dist/resources.js +18 -9
  46. package/dist/server.js +4 -0
  47. package/dist/thread.js +15 -14
  48. package/dist/tools/common.js +8 -3
  49. package/dist/tools/download.d.ts +2 -0
  50. package/dist/tools/download.js +38 -0
  51. package/dist/tools/identity.js +1 -1
  52. package/dist/tools/list.js +3 -3
  53. package/dist/tools/read.js +26 -43
  54. package/dist/tools/save.d.ts +2 -0
  55. package/dist/tools/save.js +84 -0
  56. package/dist/tools/send.js +9 -13
  57. package/dist/tools/wait.js +8 -6
  58. package/dist/wait.d.ts +1 -1
  59. package/dist/wait.js +161 -67
  60. package/docs/http-deployment.md +81 -0
  61. package/docs/oauth.md +169 -0
  62. package/docs/token-providers.md +94 -0
  63. package/package.json +8 -1
  64. package/server.json +54 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ - Add HTTP-only `get_attachment_download_url` and authenticated binary attachment GETs. Links
6
+ contain no credentials; downloads reuse caller authentication and OAuth read scope, stream the
7
+ original bytes, and keep Web API access checks authoritative. API-key deployments configure
8
+ `FMSG_MCP_PUBLIC_URL`; OAuth reuses its resource URL. Inline downloads and stdio saves remain available.
9
+ - Abort failed response streams so a truncated download cannot appear successful.
10
+ - Close unused replacement connections on HTTP shutdown after cancelled downloads.
11
+
12
+ ## 0.2.0 (unreleased)
13
+
14
+ This is the next planned release; publication still happens through a `v0.2.0` GitHub release.
15
+
16
+ ### Breaking changes
17
+
18
+ - `download_attachment` no longer accepts `save_to` or returns `saved_to`. It is read-only. Enable
19
+ the separate stdio-only `save_attachment` tool with `FMSG_MCP_DOWNLOAD_DIR` for direct streaming
20
+ to generated filenames; it never accepts a destination path or overwrites an existing file.
21
+ - Non-loopback HTTP binds require `FMSG_MCP_ALLOWED_HOSTS`. Browser origin entries must include
22
+ scheme and port. Loopback browser origins work automatically on loopback binds unless an explicit
23
+ list is set.
24
+ - Upstream API URLs require HTTPS outside loopback unless `FMSG_ALLOW_INSECURE_HTTP=1` explicitly
25
+ enables a trusted private development network. Authenticated redirects are refused.
26
+ - Text attachments return readable text; images return one image block rather than also duplicating
27
+ the image in an embedded resource. The default inline budget is 256 KiB; callers can raise it explicitly.
28
+
29
+ ### Fixes and improvements
30
+
31
+ - Add opt-in, vendor-neutral HTTP OAuth with protected-resource discovery, signed JWT validation,
32
+ per-tool scopes and authenticated RFC 8693 exchange. Isolate caches per incoming token, cap
33
+ upstream credentials at five minutes and reconnect waits on expiry. Preserve API-key mode.
34
+ Deployed IdP and actual hosted-client acceptance remain rollout checks.
35
+
36
+ - Accept caller-bound `TokenProvider` implementations in the client library alongside API keys.
37
+ Share renewal across concurrent requests, pin the address, bound acquisition time, and propagate
38
+ cancellation. Cap early renewal for short-lived tokens and reuse renewal after late 401 responses.
39
+ This provides the credential lifecycle used by API keys and HTTP OAuth.
40
+ - Retry protected reads when a WebSocket announces a message before it is readable. If retries run
41
+ out, schedule a delayed inbox catch-up without requiring another push. Fix pre-cancelled waits
42
+ and preserve request deadlines.
43
+ - Stream attachment bodies with an idle timeout instead of a total download deadline. Repeated saves
44
+ create numbered files without overwriting. Registry metadata lists the optional download folder.
45
+ - Deduplicate token exchanges and close evicted/invalidated clients once active requests finish.
46
+ Request identity survives cache eviction and SDK cloning of authentication metadata.
47
+ - Keep each message body fenced separately from its escaped header, and server guidance outside
48
+ the data. Clarify authorized conversation behavior and restore reversible/idempotent reaction annotations.
49
+ - Bound inline attachment reads and error previews while streaming. Preserve the host's canonical
50
+ JSON 400/413 policy explanations and per-recipient delivery codes, except selected secret redaction.
51
+ - Surface invalid stdio configuration through discoverable tools with corrective guidance.
52
+ - `FmsgClient.send()` reports `redactions` and the transmitted `topic`; selected credential formats
53
+ in bodies/topics are replaced once at the client boundary. Attachments remain unchanged.
54
+ - Custom HTTP adapters using `ApiKeyCallerProvider` must call `release(authInfo)` when each verified
55
+ request finishes; the built-in HTTP adapter handles this automatically.
56
+
57
+ Messaging permissions and quotas remain in fmsg-webapi. No additional MCP messaging approval flow
58
+ is introduced. See [GitHub releases](https://github.com/markmnl/fmsg-mcp/releases) for earlier notes.
package/README.md CHANGED
@@ -7,23 +7,48 @@
7
7
  An [MCP](https://modelcontextprotocol.io) server that gives any AI agent its own
8
8
  [fmsg](https://github.com/markmnl/fmsg) address: send messages, follow threads, react, exchange
9
9
  attachments and wait for replies, through a deployed
10
- [fmsg Web API](https://github.com/markmnl/fmsg-webapi). Works with Claude Code, Claude Desktop,
11
- Cursor, VS Code, claude.ai remote connectors and any other MCP host.
10
+ [fmsg Web API](https://github.com/markmnl/fmsg-webapi). Connect through stdio in hosts such as
11
+ Claude Code, Claude Desktop, Cursor and VS Code, or through HTTP using API-key headers or configured OAuth.
12
+
13
+ Use your choice of fmsg hosting provider, or self-host. No hosting provider or identity provider
14
+ is built in.
12
15
 
13
16
  - **stdio** for local hosts: one address per server process, configured by two environment variables.
14
17
  - **Streamable HTTP** for shared or remote deployments: one endpoint serving many users, each
15
- authenticated by their own fmsg API key.
18
+ authenticated by their own fmsg API key or OAuth connection.
16
19
  - The fmsg Web API client is exported for reuse: `import { FmsgClient } from "@markmnl/fmsg-mcp/client"`.
17
20
 
18
- ## 1. Get an fmsg address and API key
21
+ ## 1. Choose a connection
22
+
23
+ ### OAuth: connect and sign in
24
+
25
+ Get an OAuth-enabled MCP URL from your fmsg hosting provider, add it to your AI host, and sign in.
26
+ You do not need an API key or a local installation for this connection. Your AI host must support
27
+ the authorization server's client registration method; see [OAuth onboarding](docs/oauth.md#discovery-and-client-onboarding).
28
+ Operators can enable this with [HTTP OAuth](docs/oauth.md).
29
+
30
+ ### API key: use locally or over HTTP
19
31
 
20
32
  You send as an fmsg address, authenticated by an API key (`fmsgk_…`) issued by your fmsg host:
21
33
 
22
- - **No host yet?** Create an account at a public fmsg host such as [fmsg.io](https://fmsg.io) and
23
- add an agent (sub-account) to get an API URL and key.
34
+ - **Using a hosting provider?** Create an account with an fmsg hosting provider and obtain an
35
+ API URL and API key for the address your agent will use.
24
36
  - **Self-hosting?** Run the stack with [fmsg-docker](https://github.com/markmnl/fmsg-docker) and issue
25
37
  a key with `fmsg-webapi api-key create`.
26
38
 
39
+ Use the API URL and key for local installation below, or use your provider's API-key MCP endpoint
40
+ with an `Authorization: Bearer fmsgk_...` header. The endpoint must use the fmsg Web API that accepts
41
+ your key.
42
+
43
+ ### MCP Registry
44
+
45
+ Find `io.github.markmnl/fmsg-mcp` in the [official MCP Registry](https://registry.modelcontextprotocol.io/?q=io.github.markmnl%2Ffmsg-mcp).
46
+ The listing offers local npm installation and configurable remote connections for OAuth or API keys.
47
+ For a remote connection, supply the MCP endpoint from your fmsg hosting provider or your own deployment;
48
+ the registry input takes its hostname and path without the `https://` prefix, such as `mcp.example.com/mcp`.
49
+ OAuth sign-in is discovered from that endpoint; the API-key option additionally asks for your key.
50
+ If your AI host cannot configure registry URL templates, add the full MCP URL directly instead.
51
+
27
52
  ## 2. Install
28
53
 
29
54
  Requires Node.js 22 or later.
@@ -65,18 +90,36 @@ Run one server for many users. Each client sends **its own** fmsg API key as a b
65
90
  server exchanges it at the fmsg host and acts as that address. `FMSG_API_KEY` must not be set.
66
91
 
67
92
  ```sh
68
- FMSG_API_URL=https://api.example.com npx -y @markmnl/fmsg-mcp --http 0.0.0.0:8765
93
+ FMSG_API_URL=https://api.example.com FMSG_MCP_ALLOWED_HOSTS=mcp.example.com \
94
+ npx -y @markmnl/fmsg-mcp --http 0.0.0.0:8765
69
95
  # or
70
- docker build -t fmsg-mcp . && docker run -e FMSG_API_URL=https://api.example.com -p 8765:8765 fmsg-mcp
96
+ docker build -t fmsg-mcp .
97
+ docker run -e FMSG_API_URL=https://api.example.com \
98
+ -e FMSG_MCP_ALLOWED_HOSTS=mcp.example.com -p 8765:8765 fmsg-mcp
71
99
  ```
72
100
 
73
- The MCP endpoint is `/mcp`; `/healthz` reports liveness. Point a host at it with
74
- `Authorization: Bearer fmsgk_...` — for claude.ai, add a custom connector with that URL and header;
75
- for Claude Code, `claude mcp add --transport http fmsg https://mcp.example.com/mcp --header "Authorization: Bearer fmsgk_..."`.
101
+ The MCP endpoint is `/mcp`; `/healthz` reports liveness. Use a client that supports an explicitly
102
+ configured `Authorization: Bearer fmsgk_...` header. Each caller supplies its own key; a shared
103
+ header means a shared fmsg identity. For browser sign-in without user API keys, configure
104
+ [HTTP OAuth](docs/oauth.md): the operator supplies an issuer, resource URI and exchange client.
105
+ Users add the public MCP URL in a host supporting that issuer's client registration method.
106
+
107
+ For [Claude Code over HTTP](https://code.claude.com/docs/en/mcp):
108
+
109
+ ```sh
110
+ claude mcp add --transport http fmsg --scope user https://mcp.example.com/mcp \
111
+ --header "Authorization: Bearer fmsgk_..."
112
+ ```
76
113
 
77
114
  Deploy behind a TLS-terminating reverse proxy and set `FMSG_MCP_ALLOWED_HOSTS` to the public hostname
78
- when binding to a non-loopback address. `wait_for_message` holds a request open for up to
115
+ when binding to a non-loopback address; startup fails without it. Browser clients on another origin
116
+ also need `FMSG_MCP_ALLOWED_ORIGINS` containing exact origins, such as `https://app.example.com`.
117
+ For loopback binds, loopback browser origins on any port work by default, including MCP Inspector
118
+ at `http://localhost:6274`. Setting an explicit origin list replaces that loopback default.
119
+ Allowed preflights need no credentials; actual MCP requests always require authentication.
120
+ `wait_for_message` holds a request open for up to
79
121
  `FMSG_MCP_WAIT_MAX_SECONDS` (230), so give the proxy an idle timeout of at least 240 s.
122
+ See the [TLS reverse-proxy example](docs/http-deployment.md) for a loopback deployment with Caddy.
80
123
 
81
124
  ## Tools
82
125
 
@@ -93,7 +136,9 @@ when binding to a non-loopback address. `wait_for_message` holds a request open
93
136
  | `add_recipients` | Add recipients to a sent message |
94
137
  | `react` | Set or clear your emoji reaction |
95
138
  | `mark_read` | Mark received messages read |
96
- | `download_attachment` | Fetch an attachment inline (base64, images as image blocks) or, over stdio, save it to disk |
139
+ | `download_attachment` | Fetch a small attachment inline: text as text, images as image blocks, other files as base64 resources |
140
+ | `get_attachment_download_url` | Get a credential-free URL and resource link for an authenticated binary download; HTTP only, requires a public MCP URL |
141
+ | `save_attachment` | Stream an attachment to the configured local folder; stdio only, enabled by `FMSG_MCP_DOWNLOAD_DIR` |
97
142
  | `delivery_status` | Per-recipient delivery times and host response codes |
98
143
  | `wait_for_message` | Block until the next inbound message (WebSocket push), batched per thread, with thread context |
99
144
 
@@ -109,30 +154,68 @@ attach resources; prompts `chat` and `reply` script the wait → reply loop and
109
154
  |---|---|---|
110
155
  | `FMSG_API_URL` | — | Base URL of the fmsg Web API (required) |
111
156
  | `FMSG_API_KEY` | — | `fmsgk_…` key; stdio mode only |
157
+ | `FMSG_MCP_AUTH_MODE` | `api-key` | HTTP authentication: `api-key` or `oauth`; see [OAuth settings](docs/oauth.md#operator-configuration) |
158
+ | `FMSG_MCP_PUBLIC_URL` | OAuth resource URL, otherwise unset | Public MCP endpoint, including `/mcp`; enables the HTTP download-link tool. HTTPS required except loopback; in OAuth mode must equal `FMSG_MCP_OAUTH_RESOURCE_URL` |
159
+ | `FMSG_ALLOW_INSECURE_HTTP` | disabled | Set to `1` only to permit cleartext API access on a trusted development/private network; loopback HTTP is allowed by default |
112
160
  | `FMSG_DEFAULT_DOMAIN` | — | Lets short names resolve: `bob` → `@bob@<domain>` |
113
161
  | `FMSG_DIRECTORY` | — | JSON file mapping short names to full addresses |
162
+ | `FMSG_MCP_DOWNLOAD_DIR` | — | Enable `save_attachment` in stdio; folder for new files named from message ID and filename |
114
163
  | `FMSG_MCP_WAIT_MAX_SECONDS` | `230` | Cap on one `wait_for_message` call |
115
- | `FMSG_MCP_DOWNLOAD_DIR` | — | Restrict `download_attachment` `save_to` to this directory (stdio) |
116
164
  | `FMSG_MCP_HOST` / `FMSG_MCP_PORT` | `127.0.0.1` / `8765` | HTTP bind address (or `--http host:port`) |
117
- | `FMSG_MCP_ALLOWED_HOSTS` | loopback names | Comma-separated `Host` header allowlist for HTTP mode |
118
- | `FMSG_MCP_ALLOWED_ORIGINS` | same as hosts | `Origin` allowlist for browser-based callers |
119
- | `FMSG_MCP_KEY_CACHE_MAX` / `FMSG_MCP_KEY_CACHE_TTL_SECONDS` | `500` / `1800` | HTTP mode per-key client cache |
165
+ | `FMSG_MCP_ALLOWED_HOSTS` | loopback names | Comma-separated `Host` header allowlist; required for non-loopback binds |
166
+ | `FMSG_MCP_ALLOWED_ORIGINS` | same origin; loopback origins on loopback binds | Comma-separated browser origins including scheme and port; an explicit list replaces the loopback default; hostname-only values are rejected |
167
+ | `FMSG_MCP_KEY_CACHE_MAX` / `FMSG_MCP_KEY_CACHE_TTL_SECONDS` | `500` / `1800` | HTTP client cache bound; TTL applies only to API-key mode |
120
168
 
121
169
  The API key is exchanged for a short-lived access token that the server renews automatically.
122
-
123
- Over stdio the server also starts with no credentials at all, so hosts and directories can list its tools; every tool call then returns a message naming the missing variables.
170
+ API URLs must not contain credentials, query strings or fragments. Authenticated requests do not
171
+ follow redirects; configure the final API URL directly.
172
+
173
+ To save attachments directly to disk, add `FMSG_MCP_DOWNLOAD_DIR` to your stdio server's environment,
174
+ for example `/home/you/Downloads/fmsg`. The optional `save_attachment` tool streams files into that
175
+ folder without sending their bytes through model context. It accepts only a message ID and attachment
176
+ filename and creates a new file such as `123-report.pdf`. Repeat saves use `123-report-1.pdf`,
177
+ `123-report-2.pdf`, etc., leaving existing files untouched. Unusual filenames are converted to
178
+ portable names; use the returned `saved_to` path. Streaming downloads can run longer than 60 seconds
179
+ while making progress; a 60-second idle timeout detects stalled transfers.
180
+
181
+ Inline downloads default to 256 KiB to keep file content manageable for the model. Use
182
+ `save_attachment` for larger local files, or raise `max_inline_bytes` explicitly when your AI host
183
+ can handle more inline content.
184
+
185
+ For remote file downloads, call `get_attachment_download_url` with the message ID and filename.
186
+ It returns metadata and an HTTPS `resource_link`; your AI host downloads the original bytes using
187
+ the existing MCP connection's Authorization header. API-key operators enable the tool with
188
+ `FMSG_MCP_PUBLIC_URL=https://mcp.example.com/mcp`; OAuth deployments reuse their configured resource
189
+ URL automatically. The [reverse proxy must forward the attachment route](docs/http-deployment.md#binary-attachment-downloads).
190
+ Links contain no credentials and grant no access by themselves. The host must support authenticated
191
+ HTTP downloads; an ordinary browser click without the header returns 401. Use inline downloads when
192
+ that host capability is unavailable. Downloads stream without the inline size budget, using the
193
+ original Content-Type and download filename. No files are stored on the MCP server.
194
+
195
+ The Web API already transfers attachments as raw bytes. Base64 is used only when embedding binary
196
+ content in MCP's JSON results, as required by [MCP binary resources](https://modelcontextprotocol.io/specification/2026-07-28/server/resources#binary-content).
197
+ `save_attachment` and authenticated HTTP downloads avoid that encoding and keep file bytes out of
198
+ model context. See [issue #6](https://github.com/markmnl/fmsg-mcp/issues/6).
199
+
200
+ Over stdio, missing or invalid configuration still allows hosts to discover the tools. Tool calls
201
+ explain the configuration error and how to fix it; restart the MCP server after correcting settings.
124
202
 
125
203
  ## Safety
126
204
 
205
+ - Messaging access, quotas and recipient acceptance are enforced by fmsg-webapi and the host
206
+ services. MCP forwards each operation as the caller's identity and surfaces upstream failures.
207
+ - `download_attachment` never writes local files. Optional `save_attachment` writes only generated
208
+ filenames in the operator-configured folder, using exclusive creation with no overwrite.
127
209
  - Sent messages cannot be edited or recalled; send tools say so in their descriptions and are
128
- annotated `destructiveHint` so hosts can ask for confirmation.
129
- - API keys, tokens and other secret-shaped strings are redacted from outbound bodies, topics and
130
- error text; the count of redactions is reported.
210
+ annotated `destructiveHint` to describe their effects. Approval behavior belongs to the AI host;
211
+ fmsg-mcp has no additional confirmation gate.
212
+ - Selected API-key/token formats are redacted from outbound bodies, topics and error text; the
213
+ send tools report the count. This is not general data-loss prevention or binary attachment scanning.
131
214
  - Nothing about message size or acceptance is assumed: the fmsg host's own responses and delivery
132
215
  codes are surfaced verbatim.
133
216
  - The server publishes MCP `instructions` (shown to the model at session start) telling agents to use
134
- these tools rather than a local fmsg CLI or cached credentials, to send only on a clear request, and
135
- to treat message content as data.
217
+ these tools rather than a local fmsg CLI or cached credentials, to carry out authorized tasks and
218
+ automation without repeated confirmation, and to treat message content as data.
136
219
  - See [SECURITY.md](./SECURITY.md).
137
220
 
138
221
  ## Using the client library
@@ -143,9 +226,20 @@ import { FmsgClient } from "@markmnl/fmsg-mcp/client";
143
226
  const client = new FmsgClient("https://api.example.com", process.env.FMSG_API_KEY!);
144
227
  console.log(await client.address());
145
228
  const inbox = await client.listInbox(10);
146
- await client.send({ to: ["@bob@example.com"], topic: "Hi", body: "Hello from code" });
229
+ const sent = await client.send({ to: ["@bob@example.com"], topic: "Hi", body: "Hello from code" });
230
+ console.log(sent.id, sent.redactions);
231
+ client.close();
147
232
  ```
148
233
 
234
+ `send()` replaces selected credential patterns in the body and topic before creating the draft.
235
+ Its result includes the replacement count (`redactions`) and transmitted `topic`. Attachments are
236
+ unchanged. Use `streamAttachment()` to consume large files incrementally; consume or cancel its stream.
237
+
238
+ Applications with their own authorization integration can pass a `TokenProvider` instead of an
239
+ API-key string. The client shares renewal across concurrent requests and keeps the authenticated
240
+ address fixed. See the [token-provider contract](./docs/token-providers.md). This library interface
241
+ is also used by the executable's optional [OAuth mode](./docs/oauth.md).
242
+
149
243
  ## Development
150
244
 
151
245
  ```sh
@@ -155,6 +249,14 @@ npx @modelcontextprotocol/inspector node dist/index.js # stdio, with FM
155
249
  bash .github/scripts/run-fmsg-docker-e2e.sh # end to end on two real fmsg stacks
156
250
  ```
157
251
 
158
- See [AGENTS.md](./AGENTS.md) for layout and conventions.
252
+ See [AGENTS.md](./AGENTS.md) for layout and conventions, [ROADMAP.md](./ROADMAP.md) for remaining
253
+ integration work, and [CHANGELOG.md](./CHANGELOG.md) for release notes.
254
+
255
+ ### Releasing
256
+
257
+ Publish a stable GitHub release tagged `vX.Y.Z` to run the checks, publish the npm package, and
258
+ then update the MCP Registry with the same version. Both use OIDC authentication; no dedicated
259
+ registry secret is needed. Drafts and prereleases do not publish. Registry publishing runs as a
260
+ separate job, so if it fails after npm succeeds, use **Re-run failed jobs** on the release workflow.
159
261
 
160
262
  [MIT licensed](./LICENSE)
package/ROADMAP.md ADDED
@@ -0,0 +1,52 @@
1
+ # Integration roadmap
2
+
3
+ Target: an MCP-capable agent can connect through a documented, tested path, identify its fmsg
4
+ account, and complete authorized messaging without exposing credentials or unexpected capabilities.
5
+ Support claims must name tested clients and versions; agents without MCP need an adapter.
6
+
7
+ ## Constraints
8
+
9
+ - fmsg-webapi and host services own messaging permissions, visibility, quotas and acceptance.
10
+ MCP binds each operation to its caller and surfaces upstream decisions.
11
+ - Normal stdio setup requires only an HTTPS API URL and key. No second login, duplicate ACLs,
12
+ recipient policies or per-message confirmation gates. The AI host owns tool approval settings.
13
+ - Keep authorized conversations and automation convenient. Incoming messages do not authorize
14
+ adding recipients, contacting new parties or disclosing other data.
15
+ - Remove obsolete server API fields directly while there are no external consumers requiring
16
+ compatibility. Keep protocol compatibility needed by supported MCP hosts.
17
+
18
+ ## Implementation sequence
19
+
20
+ | Work | Scope and completion criteria |
21
+ |---|---|
22
+ | A — MCP boundaries ([PR #2](https://github.com/markmnl/fmsg-mcp/pull/2)) | Separate read-only downloads from opt-in streamed saving; validate HTTP access; manage caller credentials and cancellation; preserve upstream authorization; frame untrusted content without obscuring server guidance. Regression and real-stack isolation tests cover these boundaries. |
23
+ | B — Receive reliability | Scan backlogs to a safe cursor boundary, including bursts, pending batches, interleaved threads and reconnects. Never advance past unseen work. Bound stream reads, response assembly, concurrent waits and overall deadlines. |
24
+ | C — Action outcomes | Preserve upstream denials and delivery codes. Return a durable reference and recovery guidance when a send may have committed but its response was lost; coordinate idempotency with the upstream API. |
25
+ | D — Painless local integration | Add a non-sending doctor command, separate host recipes and a versioned compatibility matrix. Verify wait defaults, attachment save/upload workflows, independent Python clients, conformance, supported OSes and clean installation of the actual npm tarball. |
26
+ | E — Hosted OAuth | Provider-neutral discovery, account linkage, consent, token refresh and revocation, coordinated with the host/account system. Prove per-user isolation in actual hosted clients. Retain explicit API-key integration. |
27
+ | F — Release trust and operations | Synchronize the existing MCP Registry listing after npm publication; verify the published version. Harden release inputs and gates, reusing D's artifact checks. Add deployment metrics, runbooks, load testing and independent review when supporting shared hosted service. |
28
+
29
+ Ship A first, then B–D. Plan E with the host/account-system maintainer. Release work in F can proceed
30
+ earlier; hosted-service promises depend on verified OAuth and operational behavior.
31
+
32
+ E's [HTTP OAuth implementation](docs/oauth.md) includes discovery, incoming JWT validation, tool
33
+ scopes, token exchange, isolated renewal and expiring-socket reconnects. Local signed-token tests
34
+ cover these boundaries. Deployed IdP/Web API and hosted-client acceptance remain open. The Web API
35
+ must enforce delegated scopes and closed owner routes before OAuth is enabled; messaging
36
+ credentials must not inherit owner credential-management privileges.
37
+
38
+ Release-triggered npm publication, OIDC trusted publishing, provenance generation and version
39
+ synchronization already exist in [publish.yml](.github/workflows/publish.yml). Preserve them.
40
+ [CI](https://github.com/markmnl/fmsg-mcp/actions/workflows/tests.yml) already covers Node 22/24,
41
+ the Docker image and real two-host acceptance. PR checks record validation for each revision.
42
+
43
+ ## Broad-integration release criteria
44
+
45
+ - Fresh installs on every claimed client/OS reach `whoami` and inbox using the documented setup.
46
+ - Caller isolation and upstream authorization hold across tools, resources, attachments and waits.
47
+ - Backlog/reconnect tests prove no silent cursor loss; cancellation releases work promptly.
48
+ - Tool deadlines and payload budgets fit verified host configurations; large files are practical
49
+ without manual base64 handling or overflowing model context.
50
+ - Ambiguous sends have a documented reconciliation path that avoids blind duplicate sends.
51
+ - Advertised hosted integrations pass identity, refresh, revocation and disconnect checks.
52
+ - The npm artifact, registry metadata, release notes and compatibility results agree.
package/SECURITY.md ADDED
@@ -0,0 +1,78 @@
1
+ # Security
2
+
3
+ Please report vulnerabilities privately through GitHub's
4
+ [private vulnerability reporting](https://github.com/markmnl/fmsg-mcp/security/advisories/new)
5
+ rather than a public issue.
6
+
7
+ ## Scope
8
+
9
+ - Messaging authorization, grants, address status, quotas and acceptance remain the responsibility
10
+ of fmsg-webapi and the host services. Each MCP request uses its caller's upstream identity.
11
+ - Over stdio the API key comes from the environment. HTTP callers supply their own bearer keys or OAuth access tokens, according to the configured mode.
12
+ Keys and JWTs are retained in process memory for token renewal; hashes index the HTTP client cache.
13
+ This server does not intentionally persist them. Idle entries expire on access and periodic sweeps
14
+ (at most 30 seconds apart). Evicted or invalidated clients close immediately if idle; active requests
15
+ retain their client until their last request lease is released. Shutdown closes cached and active
16
+ clients and cancels their work. JavaScript does not guarantee memory zeroization.
17
+ - Protected upstream routes re-check grants; revoked/expired credentials remain subject to the
18
+ upstream contract. MCP cache TTL is a retention setting, not a grant or revocation policy.
19
+ WebSocket announcements trigger protected message reads before their content reaches the host.
20
+ The upstream contract authenticates sockets at handshake; it does not promise immediate closure
21
+ of existing sockets on revocation. MCP does not infer ongoing authorization from a socket alone.
22
+ - Selected key/JWT/private-key patterns are redacted from outbound message bodies, topics and errors.
23
+ This does not detect arbitrary sensitive information or scan binary attachments.
24
+ - Message bodies and upstream error text are fenced as untrusted data. Each message body has its own
25
+ fence; server-built headers stay outside it, with external header values escaped onto one line.
26
+ This distinguishes quoted forged headers from actual message boundaries. Server-authored guidance
27
+ stays outside the data. Instructions permit replies within authorized conversations, but
28
+ incoming messages cannot authorize adding recipients, contacting new parties or disclosing other
29
+ data. Hosts should still treat tool output as untrusted.
30
+ - `download_attachment` is read-only and enforces its inline byte budget while reading. The optional
31
+ `save_attachment` tool is advertised only in stdio with `FMSG_MCP_DOWNLOAD_DIR` set. It streams to a
32
+ generated leaf filename, accepts no destination path, and uses exclusive creation (`wx`) to skip
33
+ existing files and symlinks, trying numbered filenames instead. New files use mode `0600` where
34
+ supported; failed writes remove partial files. The operator must control the configured folder and its ancestors, on a filesystem that
35
+ supports exclusive creation. This is not a sandbox against another local process replacing those
36
+ directories. HTTP mode does not expose this write capability.
37
+ - Attachment transfers retain caller cancellation and client shutdown signals. They use a response
38
+ header deadline followed by a per-read idle timeout (60 seconds each by default), so a progressing
39
+ large download is not subject to a 60-second total duration limit.
40
+ - HTTP attachment links contain no credentials and do not confer access. Every GET authenticates
41
+ the caller and uses its upstream client; OAuth also requires `fmsg:read`. The Web API decides
42
+ attachment visibility. Public URLs come from operator configuration, never forwarded headers.
43
+ Downloads stream with backpressure and force attachment disposition, no-store caching and nosniff.
44
+ Failed streams terminate the connection so partial files cannot appear successfully completed.
45
+ Query parameters are refused. Authenticated download links require support in the AI host.
46
+ - Error previews are limited to 2 KiB while reading, except canonical JSON HTTP 400/413 responses:
47
+ those retain the host's acceptance/size-policy explanation. Selected credentials are still redacted;
48
+ oversized previews are explicitly marked as truncated.
49
+ - In HTTP mode terminate TLS in front of the server. `FMSG_MCP_ALLOWED_HOSTS` is required for
50
+ non-loopback binds. Browser access validates the exact origin, including scheme and port. Loopback
51
+ binds also permit loopback browser origins on any port by default, so local developer tools work.
52
+ An explicit origin list replaces that loopback exception. CORS preflight permission does not grant
53
+ access to MCP operations; bearer authentication remains required.
54
+ - The upstream API must use HTTPS except loopback or an explicitly configured trusted private
55
+ network (`FMSG_ALLOW_INSECURE_HTTP=1`). Authenticated HTTP redirects are refused.
56
+ - Tool annotations and message-data labels guide the AI host; they do not prove user approval or
57
+ prevent prompt injection. The AI host owns tool-use permissions and authorization of automation.
58
+ OAuth mode adds resource-token validation and messaging scope checks, with sign-in/consent at
59
+ the configured authorization server. fmsg-mcp adds no recipient ACL, approval gate or per-message
60
+ confirmation requirement. Normal stdio setup needs only an HTTPS API URL and API key; token renewal and cache
61
+ management run automatically. Guidance permits ongoing work within the user's authorized task or
62
+ automation, subject to the AI host's own approval settings.
63
+
64
+ ## OAuth boundary
65
+
66
+ OAuth mode validates the configured issuer, exact MCP audience, EdDSA signature, key ID,
67
+ `at+jwt` type, lifetime and address. Only the configured issuer's discovered JWKS is trusted.
68
+ Incoming tokens are exchanged with confidential-client authentication and are never forwarded
69
+ to the Web API. No `X-FMSG-Act-As` header is sent. The Web API must enforce delegated scopes
70
+ and refuse owner-only routes independently of this server.
71
+
72
+ Exchanged tokens are cached per incoming token for at most five minutes and never past either
73
+ token's expiry. Existing sockets close and renew within that deadline. Incoming tokens validate
74
+ offline until expiry; immediate revocation is checked at exchange. No local cache makes
75
+ revocation immediate or shortens the lifetime of a copied token at another service. See the
76
+ [OAuth contract and revocation limits](docs/oauth.md#exchange-renewal-and-revocation).
77
+
78
+ When reporting, please remove API keys, tokens, addresses and message bodies from logs.
package/dist/auth.d.ts CHANGED
@@ -2,20 +2,28 @@ import { type AuthInfo, type OAuthTokenVerifier } from "@modelcontextprotocol/se
2
2
  import type { Config } from "./config.js";
3
3
  import type { Caller, CallerProvider } from "./context.js";
4
4
  export declare const FMSG_SCOPE = "fmsg";
5
- /**
6
- * HTTP mode: each request carries an fmsg API key as its bearer token. The key
7
- * is exchanged once at the fmsg host, and the resulting client (which renews
8
- * its own JWT) is cached by the key's hash. Raw keys are never stored or logged.
9
- */
5
+ /** Per-key clients retain credentials for renewal; upstream decides access. */
10
6
  export declare class ApiKeyCallerProvider implements CallerProvider, OAuthTokenVerifier {
11
7
  private readonly config;
12
8
  private readonly log;
13
9
  private readonly entries;
10
+ private readonly pending;
11
+ private readonly pendingClients;
12
+ private readonly leases;
13
+ private readonly timer;
14
+ private closed;
14
15
  constructor(config: Config, log?: (line: string) => void);
15
16
  static cacheKey(apiKey: string): string;
17
+ private releaseEntry;
18
+ private drop;
16
19
  private evict;
17
- /** Bearer verifier for the MCP gate: exchange the key, return the caller's identity. */
20
+ private createEntry;
18
21
  verifyAccessToken(token: string): Promise<AuthInfo>;
22
+ private entryFor;
19
23
  forRequest(authInfo: AuthInfo | undefined): Promise<Caller>;
24
+ /** The HTTP adapter releases this lease when the response or connection ends. */
25
+ release(authInfo: AuthInfo): void;
26
+ invalidate(caller: Caller): void;
27
+ close(): void;
20
28
  get size(): number;
21
29
  }