@markmnl/fmsg-mcp 0.2.2 → 0.2.3
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/CHANGELOG.md +4 -0
- package/README.md +2 -1
- package/dist/client/url.d.ts +1 -1
- package/dist/client/url.js +5 -5
- package/dist/config.d.ts +6 -0
- package/dist/config.js +18 -0
- package/dist/tools/identity.js +7 -4
- package/docs/http-deployment.md +6 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Add optional `FMSG_API_PUBLIC_URL`: the Web API URL `whoami` reports to users, while requests
|
|
6
|
+
keep using `FMSG_API_URL`. It defaults to `FMSG_API_URL`, except that HTTP mode no longer
|
|
7
|
+
reports a cleartext (internal or loopback) upstream URL. `whoami`'s `api_url` is therefore
|
|
8
|
+
nullable and is `null` when no public URL is known.
|
|
5
9
|
- Add HTTP-only `get_attachment_download_url` and authenticated binary attachment GETs. Links
|
|
6
10
|
contain no credentials; downloads reuse caller authentication and OAuth read scope, stream the
|
|
7
11
|
original bytes, and keep Web API access checks authoritative. API-key deployments configure
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ See the [TLS reverse-proxy example](docs/http-deployment.md) for a loopback depl
|
|
|
125
125
|
|
|
126
126
|
| Tool | What it does |
|
|
127
127
|
|---|---|
|
|
128
|
-
| `whoami` | The address this server acts as, the API URL and token expiry |
|
|
128
|
+
| `whoami` | The address this server acts as, the public API URL (when known) and token expiry |
|
|
129
129
|
| `resolve_address` | Turn a short name into `@user@domain` (directory, then default domain) |
|
|
130
130
|
| `list_messages` | Inbox, newest first, with previews; reactions hidden; optional unread filter |
|
|
131
131
|
| `list_sent` | Sent messages with per-recipient delivery state |
|
|
@@ -154,6 +154,7 @@ attach resources; prompts `chat` and `reply` script the wait → reply loop and
|
|
|
154
154
|
|---|---|---|
|
|
155
155
|
| `FMSG_API_URL` | — | Base URL of the fmsg Web API (required) |
|
|
156
156
|
| `FMSG_API_KEY` | — | `fmsgk_…` key; stdio mode only |
|
|
157
|
+
| `FMSG_API_PUBLIC_URL` | `FMSG_API_URL` (HTTP mode: only if HTTPS) | Web API URL reported to users by `whoami`; never used for requests. Set it when the server reaches the Web API at an internal address. Without it, HTTP mode reports no URL for a cleartext `FMSG_API_URL`. Validated like `FMSG_API_URL` |
|
|
157
158
|
| `FMSG_MCP_AUTH_MODE` | `api-key` | HTTP authentication: `api-key` or `oauth`; see [OAuth settings](docs/oauth.md#operator-configuration) |
|
|
158
159
|
| `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
160
|
| `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 |
|
package/dist/client/url.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Hosts for which cleartext loopback development is safe by default. */
|
|
2
2
|
export declare function isLoopbackHost(host: string): boolean;
|
|
3
|
-
export declare function normalizeApiUrl(value: string, allowInsecureHttp?: boolean): string;
|
|
3
|
+
export declare function normalizeApiUrl(value: string, allowInsecureHttp?: boolean, name?: string): string;
|
|
4
4
|
export declare function normalizeOrigin(value: string): string;
|
package/dist/client/url.js
CHANGED
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
export function isLoopbackHost(host) {
|
|
3
3
|
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
4
4
|
}
|
|
5
|
-
export function normalizeApiUrl(value, allowInsecureHttp = false) {
|
|
5
|
+
export function normalizeApiUrl(value, allowInsecureHttp = false, name = "FMSG_API_URL") {
|
|
6
6
|
let url;
|
|
7
7
|
try {
|
|
8
8
|
url = new URL(value);
|
|
9
9
|
}
|
|
10
10
|
catch {
|
|
11
|
-
throw new Error(
|
|
11
|
+
throw new Error(`${name} must be an absolute http(s) URL`);
|
|
12
12
|
}
|
|
13
13
|
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
14
|
-
throw new Error(
|
|
14
|
+
throw new Error(`${name} must be an http(s) URL`);
|
|
15
15
|
if (url.username || url.password || url.search || url.hash) {
|
|
16
|
-
throw new Error(
|
|
16
|
+
throw new Error(`${name} must not contain credentials, a query string or a fragment`);
|
|
17
17
|
}
|
|
18
18
|
if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !allowInsecureHttp) {
|
|
19
|
-
throw new Error(
|
|
19
|
+
throw new Error(`${name} must use HTTPS outside loopback; explicitly enable FMSG_ALLOW_INSECURE_HTTP=1 only for a trusted development/private network`);
|
|
20
20
|
}
|
|
21
21
|
return url.href.replace(/\/+$/u, "");
|
|
22
22
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export type HttpConfig = {
|
|
|
15
15
|
export type Config = {
|
|
16
16
|
transport: Transport;
|
|
17
17
|
apiUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* The Web API URL shown to users (whoami). FMSG_API_PUBLIC_URL when set; otherwise FMSG_API_URL,
|
|
20
|
+
* except over HTTP where a cleartext upstream (typically a private or loopback address the
|
|
21
|
+
* server reaches internally) is not shown. Never used for requests.
|
|
22
|
+
*/
|
|
23
|
+
apiPublicUrl?: string;
|
|
18
24
|
/** Explicit opt-in for cleartext upstream traffic outside loopback. */
|
|
19
25
|
allowInsecureHttp?: boolean;
|
|
20
26
|
/** Only set in stdio mode. */
|
package/dist/config.js
CHANGED
|
@@ -39,6 +39,22 @@ function loadDirectory(path) {
|
|
|
39
39
|
}
|
|
40
40
|
return out;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the Web API URL shown to users. An explicit FMSG_API_PUBLIC_URL is validated like
|
|
44
|
+
* FMSG_API_URL. Without it, stdio shows FMSG_API_URL (the user's own configuration), while HTTP
|
|
45
|
+
* shows it only when it is HTTPS: a cleartext upstream on a hosted server is an internal address
|
|
46
|
+
* that means nothing to remote clients.
|
|
47
|
+
*/
|
|
48
|
+
function resolveApiPublicUrl(env, transport, apiUrl, allowInsecureHttp) {
|
|
49
|
+
const explicit = env.FMSG_API_PUBLIC_URL?.trim();
|
|
50
|
+
if (explicit)
|
|
51
|
+
return normalizeApiUrl(explicit, allowInsecureHttp, "FMSG_API_PUBLIC_URL");
|
|
52
|
+
if (!apiUrl)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (transport === "http" && !apiUrl.startsWith("https:"))
|
|
55
|
+
return undefined;
|
|
56
|
+
return apiUrl;
|
|
57
|
+
}
|
|
42
58
|
export function loadConfig(env, transport, overrides = {}, options = {}) {
|
|
43
59
|
const requireCredentials = options.requireCredentials ?? true;
|
|
44
60
|
const oauth = loadOAuthConfig(env, transport);
|
|
@@ -48,6 +64,7 @@ export function loadConfig(env, transport, overrides = {}, options = {}) {
|
|
|
48
64
|
}
|
|
49
65
|
const allowInsecureHttp = env.FMSG_ALLOW_INSECURE_HTTP === "1";
|
|
50
66
|
const normalizedApiUrl = apiUrl ? normalizeApiUrl(apiUrl, allowInsecureHttp) : "";
|
|
67
|
+
const apiPublicUrl = resolveApiPublicUrl(env, transport, normalizedApiUrl, allowInsecureHttp);
|
|
51
68
|
const apiKey = env.FMSG_API_KEY?.trim();
|
|
52
69
|
if (transport === "stdio" && apiKey && !apiKey.startsWith("fmsgk_"))
|
|
53
70
|
throw new Error("FMSG_API_KEY must start with fmsgk_");
|
|
@@ -71,6 +88,7 @@ export function loadConfig(env, transport, overrides = {}, options = {}) {
|
|
|
71
88
|
return {
|
|
72
89
|
transport,
|
|
73
90
|
apiUrl: normalizedApiUrl,
|
|
91
|
+
...(apiPublicUrl ? { apiPublicUrl } : {}),
|
|
74
92
|
...(oauth ? { oauth } : {}),
|
|
75
93
|
allowInsecureHttp,
|
|
76
94
|
...(transport === "stdio" && apiKey ? { apiKey } : {}),
|
package/dist/tools/identity.js
CHANGED
|
@@ -6,12 +6,13 @@ import { toolError } from "../errors.js";
|
|
|
6
6
|
export const registerIdentityTools = (server, deps) => {
|
|
7
7
|
server.registerTool("whoami", {
|
|
8
8
|
title: "Show fmsg identity",
|
|
9
|
-
description: "Report the fmsg address this server acts as (from the authenticated connection), the fmsg Web API URL
|
|
9
|
+
description: "Report the fmsg address this server acts as (from the authenticated connection), the fmsg Web API URL " +
|
|
10
|
+
"(null when the server does not publish it), " +
|
|
10
11
|
"when the current access token expires (it is renewed automatically; no action needed), and the " +
|
|
11
12
|
"address-resolution defaults. Call this first if unsure who you are sending as.",
|
|
12
13
|
outputSchema: z.object({
|
|
13
14
|
address: z.string(),
|
|
14
|
-
api_url: z.string(),
|
|
15
|
+
api_url: z.string().nullable(),
|
|
15
16
|
token_expires_at: z.string().nullable(),
|
|
16
17
|
transport: z.enum(["stdio", "http"]),
|
|
17
18
|
default_domain: z.string().nullable(),
|
|
@@ -22,14 +23,16 @@ export const registerIdentityTools = (server, deps) => {
|
|
|
22
23
|
const expires = isoTime((await caller.tokenExpiresAt()) / 1000);
|
|
23
24
|
const structured = {
|
|
24
25
|
address: caller.address,
|
|
25
|
-
api_url:
|
|
26
|
+
api_url: deps.config.apiPublicUrl ?? null,
|
|
26
27
|
token_expires_at: expires,
|
|
27
28
|
transport: deps.config.transport,
|
|
28
29
|
default_domain: deps.config.defaultDomain ?? null,
|
|
29
30
|
directory_names: Object.keys(deps.config.directory ?? {}),
|
|
30
31
|
};
|
|
31
32
|
const lines = [
|
|
32
|
-
|
|
33
|
+
deps.config.apiPublicUrl
|
|
34
|
+
? `You are **${caller.address}** on ${deps.config.apiPublicUrl} (${deps.config.transport}).`
|
|
35
|
+
: `You are **${caller.address}** (${deps.config.transport}).`,
|
|
33
36
|
`Access token expires ${expires ?? "unknown"} and is renewed automatically.`,
|
|
34
37
|
];
|
|
35
38
|
if (deps.config.defaultDomain)
|
package/docs/http-deployment.md
CHANGED
|
@@ -13,7 +13,12 @@ FMSG_MCP_ALLOWED_ORIGINS=https://mcp.example.com,https://app.example.com \
|
|
|
13
13
|
npx -y @markmnl/fmsg-mcp --http 127.0.0.1:8765
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`FMSG_API_KEY` must be unset in HTTP mode. Include only browser origins you use.
|
|
16
|
+
`FMSG_API_KEY` must be unset in HTTP mode. Include only browser origins you use.
|
|
17
|
+
|
|
18
|
+
If the server reaches the Web API at an internal address instead, for example
|
|
19
|
+
`FMSG_API_URL=http://10.0.0.5:8000` with `FMSG_ALLOW_INSECURE_HTTP=1` on a private network, also set
|
|
20
|
+
`FMSG_API_PUBLIC_URL=https://api.example.com` so `whoami` reports the URL users know. Without it,
|
|
21
|
+
`whoami` omits the API URL rather than show the internal address. Behind TLS,
|
|
17
22
|
list the public HTTPS origin explicitly: the server sees the proxy's HTTP connection and does
|
|
18
23
|
not trust forwarded headers to establish the request origin.
|
|
19
24
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markmnl/fmsg-mcp",
|
|
3
3
|
"mcpName": "io.github.markmnl/fmsg-mcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"description": "MCP server for fmsg: send and receive federated messages from any AI agent via a deployed fmsg Web API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"url": "https://github.com/markmnl/fmsg-mcp",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.3",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@markmnl/fmsg-mcp",
|
|
17
|
-
"version": "0.2.
|
|
17
|
+
"version": "0.2.3",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|