Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FMSG_API_URL=https://api.example.com

# stdio mode only: the fmsgk_... API key for the address this server sends as.
# Leave unset in HTTP mode, where each client sends its own key as a bearer token.
# Leave unset in HTTP mode, where each client sends its own bearer credential.
FMSG_API_KEY=fmsgk_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Optional: lets short names resolve, e.g. "bob" -> @[email protected]
Expand All @@ -19,3 +19,12 @@ FMSG_API_KEY=fmsgk_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#FMSG_MCP_ALLOWED_ORIGINS=https://app.example.com
# Explicit opt-in for a trusted development/private HTTP API outside loopback:
#FMSG_ALLOW_INSECURE_HTTP=1

# Optional HTTP OAuth; see docs/oauth.md. Unset FMSG_API_KEY when enabling HTTP.
#FMSG_MCP_AUTH_MODE=oauth
#FMSG_MCP_OAUTH_RESOURCE_URL=https://mcp.example.com/mcp
#FMSG_MCP_OAUTH_ISSUER_URL=https://idp.example.com/oauth
#FMSG_MCP_OAUTH_CLIENT_ID=fmsg-mcp
# Supply FMSG_MCP_OAUTH_CLIENT_SECRET through your service's secret manager.
#FMSG_MCP_OAUTH_EXCHANGE_AUDIENCE=fmsg-webapi
#FMSG_MCP_OAUTH_ADDRESS_CLAIM=sub
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ The canonical contract is the fmsg-webapi README and
src/index.ts bin entry: stdio by default, --http [host:port], --version, --help
src/config.ts env → Config; FMSG_API_KEY required for stdio and refused for HTTP
src/server.ts createFmsgMcpServer(provider, config): registration only, no I/O
src/context.ts CallerProvider: fixed caller over stdio, per-bearer-key over HTTP
src/context.ts CallerProvider: fixed caller over stdio, per-bearer-credential over HTTP
src/auth.ts HTTP bearer verifier: key hash → cached FmsgClient + address
src/oauth/ HTTP OAuth discovery, validation, scopes and token exchange
src/http.ts node:http server, /mcp + /healthz, Host/Origin allowlist, bearer gate
src/tools/*.ts one file per tool group; src/tools/common.ts has shared schemas/helpers
src/wait.ts wait_for_message engine (WebSocket first, inbox catch-up, settle batching)
Expand All @@ -54,6 +55,11 @@ test/fmsg-docker.e2e.test.ts real two-host run, gated by FMSG_E2E=1
- stdout is the stdio protocol channel: log with `console.error` only.
- Public OSS repo: never name a specific identity provider; use `example.com` in examples.

OAuth mode validates incoming tokens for the exact MCP audience and exchanges them for a
separate Web API token. Never forward the incoming JWT or send `X-FMSG-Act-As`. Keep scope
classification in `src/oauth/scopes.ts` synchronized with tools; reply needs read and write.
See `docs/oauth.md` for the vendor-neutral claims contract and deployment requirements.

## Adding a tool

1. Register it in the matching `src/tools/*.ts` (or a new file wired in `src/server.ts`) with
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ This is the next planned release; publication still happens through a `v0.2.0` G

### Fixes and improvements

- Add opt-in, vendor-neutral HTTP OAuth with protected-resource discovery, signed JWT validation,
per-tool scopes and authenticated RFC 8693 exchange. Isolate caches per incoming token, cap
upstream credentials at five minutes and reconnect waits on expiry. Preserve API-key mode.
Deployed IdP and actual hosted-client acceptance remain rollout checks.

- Accept caller-bound `TokenProvider` implementations in the client library alongside API keys.
Share renewal across concurrent requests, pin the address, bound acquisition time, and propagate
cancellation. Cap early renewal for short-lived tokens and reuse renewal after late 401 responses.
This is the OAuth foundation; hosted OAuth remains separate integration work.
This provides the credential lifecycle used by API keys and HTTP OAuth.
- Retry protected reads when a WebSocket announces a message before it is readable. If retries run
out, schedule a delayed inbox catch-up without requiring another push. Fix pre-cancelled waits
and preserve request deadlines.
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ An [MCP](https://modelcontextprotocol.io) server that gives any AI agent its own
[fmsg](https://github.com/markmnl/fmsg) address: send messages, follow threads, react, exchange
attachments and wait for replies, through a deployed
[fmsg Web API](https://github.com/markmnl/fmsg-webapi). Connect through stdio in hosts such as
Claude Code, Claude Desktop, Cursor and VS Code, or through HTTP in clients that support bearer headers.
Claude Code, Claude Desktop, Cursor and VS Code, or through HTTP using API-key headers or configured OAuth.

- **stdio** for local hosts: one address per server process, configured by two environment variables.
- **Streamable HTTP** for shared or remote deployments: one endpoint serving many users, each
authenticated by their own fmsg API key.
authenticated by their own fmsg API key or OAuth connection.
- The fmsg Web API client is exported for reuse: `import { FmsgClient } from "@markmnl/fmsg-mcp/client"`.

## 1. Get an fmsg address and API key

Connecting to an existing OAuth-enabled endpoint? Add its MCP URL to your host and sign in;
you can skip the API-key setup below. Operators can enable this with [HTTP OAuth](docs/oauth.md).

You send as an fmsg address, authenticated by an API key (`fmsgk_…`) issued by your fmsg host:

- **No host yet?** Create an account at a public fmsg host such as [fmsg.io](https://fmsg.io) and
Expand Down Expand Up @@ -75,7 +78,9 @@ docker run -e FMSG_API_URL=https://api.example.com \

The MCP endpoint is `/mcp`; `/healthz` reports liveness. Use a client that supports an explicitly
configured `Authorization: Bearer fmsgk_...` header. Each caller supplies its own key; a shared
header means a shared fmsg identity. Hosted connectors that require OAuth are not supported yet.
header means a shared fmsg identity. For browser sign-in without user API keys, configure
[HTTP OAuth](docs/oauth.md): the operator supplies an issuer, resource URI and exchange client.
Users add the public MCP URL in a host supporting that issuer's client registration method.

For [Claude Code over HTTP](https://code.claude.com/docs/en/mcp):

Expand Down Expand Up @@ -126,6 +131,7 @@ attach resources; prompts `chat` and `reply` script the wait → reply loop and
|---|---|---|
| `FMSG_API_URL` | — | Base URL of the fmsg Web API (required) |
| `FMSG_API_KEY` | — | `fmsgk_…` key; stdio mode only |
| `FMSG_MCP_AUTH_MODE` | `api-key` | HTTP authentication: `api-key` or `oauth`; see [OAuth settings](docs/oauth.md#operator-configuration) |
| `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 |
| `FMSG_DEFAULT_DOMAIN` | — | Lets short names resolve: `bob` → `@bob@<domain>` |
| `FMSG_DIRECTORY` | — | JSON file mapping short names to full addresses |
Expand All @@ -134,7 +140,7 @@ attach resources; prompts `chat` and `reply` script the wait → reply loop and
| `FMSG_MCP_HOST` / `FMSG_MCP_PORT` | `127.0.0.1` / `8765` | HTTP bind address (or `--http host:port`) |
| `FMSG_MCP_ALLOWED_HOSTS` | loopback names | Comma-separated `Host` header allowlist; required for non-loopback binds |
| `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 |
| `FMSG_MCP_KEY_CACHE_MAX` / `FMSG_MCP_KEY_CACHE_TTL_SECONDS` | `500` / `1800` | HTTP mode per-key client cache |
| `FMSG_MCP_KEY_CACHE_MAX` / `FMSG_MCP_KEY_CACHE_TTL_SECONDS` | `500` / `1800` | HTTP client cache bound; TTL applies only to API-key mode |

The API key is exchanged for a short-lived access token that the server renews automatically.
API URLs must not contain credentials, query strings or fragments. Authenticated requests do not
Expand Down Expand Up @@ -193,7 +199,7 @@ unchanged. Use `streamAttachment()` to consume large files incrementally; consum
Applications with their own authorization integration can pass a `TokenProvider` instead of an
API-key string. The client shares renewal across concurrent requests and keeps the authenticated
address fixed. See the [token-provider contract](./docs/token-providers.md). This library interface
does not enable hosted OAuth in the MCP executable yet.
is also used by the executable's optional [OAuth mode](./docs/oauth.md).

## Development

Expand Down
9 changes: 5 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ Support claims must name tested clients and versions; agents without MCP need an
Ship A first, then B–D. Plan E with the host/account-system maintainer. Release work in F can proceed
earlier; hosted-service promises depend on verified OAuth and operational behavior.

E's [client token-provider foundation](docs/token-providers.md) supports renewable upstream credentials
and local isolation tests. Incoming OAuth validation, discovery, token exchange and actual hosted-client
acceptance remain open. Coordinate exchanged-token scope enforcement with the Web API before enabling
OAuth: messaging credentials must not inherit owner credential-management privileges.
E's [HTTP OAuth implementation](docs/oauth.md) includes discovery, incoming JWT validation, tool
scopes, token exchange, isolated renewal and expiring-socket reconnects. Local signed-token tests
cover these boundaries. Deployed IdP/Web API and hosted-client acceptance remain open. The Web API
must enforce delegated scopes and closed owner routes before OAuth is enabled; messaging
credentials must not inherit owner credential-management privileges.

Release-triggered npm publication, OIDC trusted publishing, provenance generation and version
synchronization already exist in [publish.yml](.github/workflows/publish.yml). Preserve them.
Expand Down
21 changes: 18 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ rather than a public issue.

- Messaging authorization, grants, address status, quotas and acceptance remain the responsibility
of fmsg-webapi and the host services. Each MCP request uses its caller's upstream identity.
- Over stdio the API key comes from the environment. HTTP callers supply their own bearer keys.
- 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.
Keys and JWTs are retained in process memory for token renewal; hashes index the HTTP client cache.
This server does not intentionally persist them. Idle entries expire on access and periodic sweeps
(at most 30 seconds apart). Evicted or invalidated clients close immediately if idle; active requests
Expand Down Expand Up @@ -49,9 +49,24 @@ rather than a public issue.
network (`FMSG_ALLOW_INSECURE_HTTP=1`). Authenticated HTTP redirects are refused.
- Tool annotations and message-data labels guide the AI host; they do not prove user approval or
prevent prompt injection. The AI host owns tool-use permissions and authorization of automation.
fmsg-mcp adds no separate login, messaging permissions, approval gate or per-message confirmation
requirement. Normal stdio setup needs only an HTTPS API URL and API key; token renewal and cache
OAuth mode adds resource-token validation and messaging scope checks, with sign-in/consent at
the configured authorization server. fmsg-mcp adds no recipient ACL, approval gate or per-message
confirmation requirement. Normal stdio setup needs only an HTTPS API URL and API key; token renewal and cache
management run automatically. Guidance permits ongoing work within the user's authorized task or
automation, subject to the AI host's own approval settings.

## OAuth boundary

OAuth mode validates the configured issuer, exact MCP audience, EdDSA signature, key ID,
`at+jwt` type, lifetime and address. Only the configured issuer's discovered JWKS is trusted.
Incoming tokens are exchanged with confidential-client authentication and are never forwarded
to the Web API. No `X-FMSG-Act-As` header is sent. The Web API must enforce delegated scopes
and refuse owner-only routes independently of this server.

Exchanged tokens are cached per incoming token for at most five minutes and never past either
token's expiry. Existing sockets close and renew within that deadline. Incoming tokens validate
offline until expiry; immediate revocation is checked at exchange. No local cache makes
revocation immediate or shortens the lifetime of a copied token at another service. See the
[OAuth contract and revocation limits](docs/oauth.md#exchange-renewal-and-revocation).

When reporting, please remove API keys, tokens, addresses and message bodies from logs.
10 changes: 7 additions & 3 deletions docs/http-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

This example runs Caddy and fmsg-mcp on the same machine. Replace `mcp.example.com` with your DNS
name pointing to that machine; Caddy needs access to ports 80/443 for automatic public TLS.
Keep port 8765 bound to loopback. Each MCP client supplies its own fmsg API key.
Keep port 8765 bound to loopback. The command below uses API keys; for OAuth add the
[OAuth configuration](oauth.md#operator-configuration).

```sh
FMSG_API_URL=https://api.example.com \
Expand All @@ -19,7 +20,8 @@ Save this as `Caddyfile`:

```caddyfile
mcp.example.com {
handle /mcp {
@fmsg path /mcp /.well-known/oauth-protected-resource /.well-known/oauth-protected-resource/*
handle @fmsg {
reverse_proxy 127.0.0.1:8765 {
transport http {
response_header_timeout 240s
Expand All @@ -46,4 +48,6 @@ allow response idle time beyond `FMSG_MCP_WAIT_MAX_SECONDS` with assembly headro
allowed and denied Host/Origin requests, unauthenticated 401 responses, CORS preflight, a read,
and cancellation through the actual deployed proxy before advertising that deployment.

This is an API-key deployment recipe. Per-user hosted OAuth onboarding is a separate workstream.
The metadata routes are public in OAuth mode and must reach the server for MCP authorization
discovery. In API-key mode they return 404. When OAuth is configured, its resource URL supplies
the public same-origin value, so explicitly listing that origin is optional.
Loading
Loading