sendmux-mcp 1.0.0__tar.gz
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.
- sendmux_mcp-1.0.0/.gitignore +17 -0
- sendmux_mcp-1.0.0/PKG-INFO +73 -0
- sendmux_mcp-1.0.0/README.md +62 -0
- sendmux_mcp-1.0.0/evals.xml +42 -0
- sendmux_mcp-1.0.0/pyproject.toml +37 -0
- sendmux_mcp-1.0.0/sendmux_mcp/__init__.py +6 -0
- sendmux_mcp-1.0.0/sendmux_mcp/__main__.py +6 -0
- sendmux_mcp-1.0.0/sendmux_mcp/cli.py +87 -0
- sendmux_mcp-1.0.0/sendmux_mcp/config.py +111 -0
- sendmux_mcp-1.0.0/sendmux_mcp/curation.py +338 -0
- sendmux_mcp-1.0.0/sendmux_mcp/openapi/__init__.py +1 -0
- sendmux_mcp-1.0.0/sendmux_mcp/openapi/openapi-app.json +15076 -0
- sendmux_mcp-1.0.0/sendmux_mcp/openapi/openapi-sending.json +832 -0
- sendmux_mcp-1.0.0/sendmux_mcp/py.typed +1 -0
- sendmux_mcp-1.0.0/sendmux_mcp/retry.py +130 -0
- sendmux_mcp-1.0.0/sendmux_mcp/security.py +67 -0
- sendmux_mcp-1.0.0/sendmux_mcp/server.py +59 -0
- sendmux_mcp-1.0.0/sendmux_mcp/smoke.py +70 -0
- sendmux_mcp-1.0.0/sendmux_mcp/specs.py +82 -0
- sendmux_mcp-1.0.0/sendmux_mcp/verification.py +21 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.tmp/
|
|
4
|
+
coverage/
|
|
5
|
+
.DS_Store
|
|
6
|
+
*.log
|
|
7
|
+
*.codegen.json
|
|
8
|
+
.codegen/
|
|
9
|
+
.codegraph/
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
packages/ts/cli/oclif.manifest.json
|
|
12
|
+
packages/ts/cli/tmp/
|
|
13
|
+
.phpunit.result.cache
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
vendor/
|
|
17
|
+
.bundle/
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sendmux-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Curated MCP servers for the Sendmux API surfaces.
|
|
5
|
+
Project-URL: Repository, https://github.com/Sendmux/sendmux-sdk
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: fastmcp==3.3.1
|
|
8
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
9
|
+
Requires-Dist: sendmux-core<2.0.0,>=1.0.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Sendmux MCP
|
|
13
|
+
|
|
14
|
+
Curated Model Context Protocol servers for the Sendmux mailbox, management, and sending API surfaces.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install sendmux-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For local development from this repository:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
OPENAPI_INPUT_DIR=/path/to/sendmux-docs pnpm build:mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Run
|
|
29
|
+
|
|
30
|
+
Each surface is a separate server so credentials and tools stay isolated.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
SENDMUX_API_KEY=smx_mbx_... sendmux-mcp-mailbox
|
|
34
|
+
SENDMUX_API_KEY=smx_root_... sendmux-mcp-management
|
|
35
|
+
SENDMUX_API_KEY=smx_root_... sendmux-mcp-sending
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use HTTP transport for hosted or remote clients. HTTP requires a separate MCP bearer token unless you explicitly opt out.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
SENDMUX_API_KEY=smx_mbx_... \
|
|
42
|
+
SENDMUX_MCP_HTTP_BEARER_TOKEN=local-mcp-token \
|
|
43
|
+
sendmux-mcp-mailbox --transport http --host 127.0.0.1 --port 8765
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The MCP endpoint defaults to `/mcp`; `/health` returns a small JSON health response.
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
| Setting | Environment | Default |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| API key | `SENDMUX_API_KEY` | required |
|
|
53
|
+
| App API base URL | `SENDMUX_APP_BASE_URL` | `https://app.sendmux.ai/api/v1` |
|
|
54
|
+
| Sending API base URL | `SENDMUX_SENDING_BASE_URL` | `https://smtp.sendmux.ai/api/v1` |
|
|
55
|
+
| Transport | `SENDMUX_MCP_TRANSPORT` | `stdio` |
|
|
56
|
+
| HTTP host | `SENDMUX_MCP_HOST` | `127.0.0.1` |
|
|
57
|
+
| HTTP port | `SENDMUX_MCP_PORT` | `8765` |
|
|
58
|
+
| HTTP path | `SENDMUX_MCP_PATH` | `/mcp` |
|
|
59
|
+
| HTTP bearer token | `SENDMUX_MCP_HTTP_BEARER_TOKEN` | required for HTTP |
|
|
60
|
+
| Allowed browser origins | `SENDMUX_MCP_ALLOWED_ORIGINS` | no browser origins |
|
|
61
|
+
| Snapshot directory override | `SENDMUX_MCP_OPENAPI_INPUT_DIR` or `OPENAPI_INPUT_DIR` | packaged snapshots |
|
|
62
|
+
| App snapshot override | `SENDMUX_MCP_APP_OPENAPI` | packaged app snapshot |
|
|
63
|
+
| Sending snapshot override | `SENDMUX_MCP_SENDING_OPENAPI` | packaged sending snapshot |
|
|
64
|
+
|
|
65
|
+
Packaged OpenAPI snapshots are the default so released tool names, schemas, and descriptions do not drift. Path, directory, and URL overrides are available for development, canary, and debugging runs.
|
|
66
|
+
|
|
67
|
+
## Tool Surfaces
|
|
68
|
+
|
|
69
|
+
- Mailbox: message read/send, threads, folders, identity, and mailbox state tools. Requires an `smx_mbx_` key.
|
|
70
|
+
- Management: domains, mailboxes, logs, metrics, and webhook tools. Requires an `smx_root_` key.
|
|
71
|
+
- Sending: send and batch send tools. Requires an `smx_root_` key.
|
|
72
|
+
|
|
73
|
+
The server rejects keys with the wrong prefix before starting.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Sendmux MCP
|
|
2
|
+
|
|
3
|
+
Curated Model Context Protocol servers for the Sendmux mailbox, management, and sending API surfaces.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sendmux-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For local development from this repository:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
OPENAPI_INPUT_DIR=/path/to/sendmux-docs pnpm build:mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Run
|
|
18
|
+
|
|
19
|
+
Each surface is a separate server so credentials and tools stay isolated.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
SENDMUX_API_KEY=smx_mbx_... sendmux-mcp-mailbox
|
|
23
|
+
SENDMUX_API_KEY=smx_root_... sendmux-mcp-management
|
|
24
|
+
SENDMUX_API_KEY=smx_root_... sendmux-mcp-sending
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use HTTP transport for hosted or remote clients. HTTP requires a separate MCP bearer token unless you explicitly opt out.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
SENDMUX_API_KEY=smx_mbx_... \
|
|
31
|
+
SENDMUX_MCP_HTTP_BEARER_TOKEN=local-mcp-token \
|
|
32
|
+
sendmux-mcp-mailbox --transport http --host 127.0.0.1 --port 8765
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The MCP endpoint defaults to `/mcp`; `/health` returns a small JSON health response.
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
| Setting | Environment | Default |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| API key | `SENDMUX_API_KEY` | required |
|
|
42
|
+
| App API base URL | `SENDMUX_APP_BASE_URL` | `https://app.sendmux.ai/api/v1` |
|
|
43
|
+
| Sending API base URL | `SENDMUX_SENDING_BASE_URL` | `https://smtp.sendmux.ai/api/v1` |
|
|
44
|
+
| Transport | `SENDMUX_MCP_TRANSPORT` | `stdio` |
|
|
45
|
+
| HTTP host | `SENDMUX_MCP_HOST` | `127.0.0.1` |
|
|
46
|
+
| HTTP port | `SENDMUX_MCP_PORT` | `8765` |
|
|
47
|
+
| HTTP path | `SENDMUX_MCP_PATH` | `/mcp` |
|
|
48
|
+
| HTTP bearer token | `SENDMUX_MCP_HTTP_BEARER_TOKEN` | required for HTTP |
|
|
49
|
+
| Allowed browser origins | `SENDMUX_MCP_ALLOWED_ORIGINS` | no browser origins |
|
|
50
|
+
| Snapshot directory override | `SENDMUX_MCP_OPENAPI_INPUT_DIR` or `OPENAPI_INPUT_DIR` | packaged snapshots |
|
|
51
|
+
| App snapshot override | `SENDMUX_MCP_APP_OPENAPI` | packaged app snapshot |
|
|
52
|
+
| Sending snapshot override | `SENDMUX_MCP_SENDING_OPENAPI` | packaged sending snapshot |
|
|
53
|
+
|
|
54
|
+
Packaged OpenAPI snapshots are the default so released tool names, schemas, and descriptions do not drift. Path, directory, and URL overrides are available for development, canary, and debugging runs.
|
|
55
|
+
|
|
56
|
+
## Tool Surfaces
|
|
57
|
+
|
|
58
|
+
- Mailbox: message read/send, threads, folders, identity, and mailbox state tools. Requires an `smx_mbx_` key.
|
|
59
|
+
- Management: domains, mailboxes, logs, metrics, and webhook tools. Requires an `smx_root_` key.
|
|
60
|
+
- Sending: send and batch send tools. Requires an `smx_root_` key.
|
|
61
|
+
|
|
62
|
+
The server rejects keys with the wrong prefix before starting.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<evals>
|
|
2
|
+
<eval id="1">
|
|
3
|
+
<question>Send a short message from the authenticated mailbox to a supplied recipient, then list recent mailbox messages to confirm the mailbox can still be read.</question>
|
|
4
|
+
<expected>Call mailbox_send_message with an idempotency key, then mailbox_list_messages with a small limit.</expected>
|
|
5
|
+
</eval>
|
|
6
|
+
<eval id="2">
|
|
7
|
+
<question>Find the latest message matching a subject fragment and read its body.</question>
|
|
8
|
+
<expected>Call mailbox_search_message_snippets, choose a message_id, then call mailbox_list_body or mailbox_list_content.</expected>
|
|
9
|
+
</eval>
|
|
10
|
+
<eval id="3">
|
|
11
|
+
<question>Show unread message volume and list the first page of messages without fetching full bodies.</question>
|
|
12
|
+
<expected>Call mailbox_count_messages, then mailbox_list_messages with a small limit.</expected>
|
|
13
|
+
</eval>
|
|
14
|
+
<eval id="4">
|
|
15
|
+
<question>Archive or mark a set of mailbox messages after the user supplies message IDs.</question>
|
|
16
|
+
<expected>Call mailbox_batch_update_messages with the supplied IDs and desired flag or folder changes.</expected>
|
|
17
|
+
</eval>
|
|
18
|
+
<eval id="5">
|
|
19
|
+
<question>Summarise the current mailbox identity and the folders available for filing messages.</question>
|
|
20
|
+
<expected>Call mailbox_get_identity and mailbox_list_folders.</expected>
|
|
21
|
+
</eval>
|
|
22
|
+
<eval id="6">
|
|
23
|
+
<question>Prepare a domain for sending and return the DNS records the user should configure.</question>
|
|
24
|
+
<expected>Call management_create_domain if needed, then management_get_domain_zone_file.</expected>
|
|
25
|
+
</eval>
|
|
26
|
+
<eval id="7">
|
|
27
|
+
<question>Create a mailbox and produce a mailbox API key for an agent that should only access that mailbox.</question>
|
|
28
|
+
<expected>Call management_create_mailbox, then management_create_mailbox_key for the new mailbox.</expected>
|
|
29
|
+
</eval>
|
|
30
|
+
<eval id="8">
|
|
31
|
+
<question>Investigate a delivery issue for a known email log ID.</question>
|
|
32
|
+
<expected>Call management_get_email_log and use the structured result to explain the delivery status.</expected>
|
|
33
|
+
</eval>
|
|
34
|
+
<eval id="9">
|
|
35
|
+
<question>Send one transactional email through the sending surface and include an idempotency key.</question>
|
|
36
|
+
<expected>Call sending_send_email with a complete message body and Idempotency-Key.</expected>
|
|
37
|
+
</eval>
|
|
38
|
+
<eval id="10">
|
|
39
|
+
<question>Explain why a mailbox-key MCP server cannot manage domains or create mailbox keys.</question>
|
|
40
|
+
<expected>List mailbox tools only; management tools are absent and management startup rejects smx_mbx_ credentials.</expected>
|
|
41
|
+
</eval>
|
|
42
|
+
</evals>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sendmux-mcp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Curated MCP servers for the Sendmux API surfaces."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastmcp==3.3.1",
|
|
13
|
+
"httpx>=0.28.1,<1",
|
|
14
|
+
"sendmux-core>=1.0.0,<2.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
sendmux-mcp = "sendmux_mcp.cli:main"
|
|
19
|
+
sendmux-mcp-mailbox = "sendmux_mcp.cli:main_mailbox"
|
|
20
|
+
sendmux-mcp-management = "sendmux_mcp.cli:main_management"
|
|
21
|
+
sendmux-mcp-sending = "sendmux_mcp.cli:main_sending"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/Sendmux/sendmux-sdk"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.sdist]
|
|
27
|
+
core-metadata-version = "2.4"
|
|
28
|
+
include = [
|
|
29
|
+
"README.md",
|
|
30
|
+
"evals.xml",
|
|
31
|
+
"sendmux_mcp",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
core-metadata-version = "2.4"
|
|
36
|
+
packages = ["sendmux_mcp"]
|
|
37
|
+
artifacts = ["sendmux_mcp/openapi/*.json"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from sendmux_mcp.config import RetryConfig, ServerConfig, Surface, config_from_env, normalise_transport, parse_csv
|
|
6
|
+
from sendmux_mcp.server import run
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv: list[str] | None = None) -> int:
|
|
10
|
+
args = parser(prog="sendmux-mcp").parse_args(argv)
|
|
11
|
+
surface = args.surface
|
|
12
|
+
if surface is None:
|
|
13
|
+
raise SystemExit("--surface is required when using sendmux-mcp")
|
|
14
|
+
run(config_from_args(surface, args))
|
|
15
|
+
return 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main_mailbox(argv: list[str] | None = None) -> int:
|
|
19
|
+
args = parser(default_surface="mailbox", prog="sendmux-mcp-mailbox").parse_args(argv)
|
|
20
|
+
run(config_from_args("mailbox", args))
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main_management(argv: list[str] | None = None) -> int:
|
|
25
|
+
args = parser(default_surface="management", prog="sendmux-mcp-management").parse_args(argv)
|
|
26
|
+
run(config_from_args("management", args))
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main_sending(argv: list[str] | None = None) -> int:
|
|
31
|
+
args = parser(default_surface="sending", prog="sendmux-mcp-sending").parse_args(argv)
|
|
32
|
+
run(config_from_args("sending", args))
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parser(*, default_surface: Surface | None = None, prog: str) -> argparse.ArgumentParser:
|
|
37
|
+
command = argparse.ArgumentParser(prog=prog)
|
|
38
|
+
if default_surface is None:
|
|
39
|
+
command.add_argument("--surface", choices=["mailbox", "management", "sending"])
|
|
40
|
+
command.add_argument("--api-key")
|
|
41
|
+
command.add_argument("--transport", choices=["stdio", "http", "streamable-http"])
|
|
42
|
+
command.add_argument("--host")
|
|
43
|
+
command.add_argument("--port", type=int)
|
|
44
|
+
command.add_argument("--path")
|
|
45
|
+
command.add_argument("--app-base-url")
|
|
46
|
+
command.add_argument("--sending-base-url")
|
|
47
|
+
command.add_argument("--openapi-input-dir")
|
|
48
|
+
command.add_argument("--app-openapi")
|
|
49
|
+
command.add_argument("--sending-openapi")
|
|
50
|
+
command.add_argument("--allowed-origin", action="append", default=[])
|
|
51
|
+
command.add_argument("--http-bearer-token")
|
|
52
|
+
command.add_argument("--allow-unauthenticated-http", action="store_true")
|
|
53
|
+
command.add_argument("--timeout-seconds", type=float)
|
|
54
|
+
command.add_argument("--retry-max-attempts", type=int)
|
|
55
|
+
command.add_argument("--retry-base-delay-seconds", type=float)
|
|
56
|
+
command.add_argument("--retry-max-delay-seconds", type=float)
|
|
57
|
+
command.set_defaults(surface=default_surface)
|
|
58
|
+
return command
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def config_from_args(surface: Surface, args: argparse.Namespace) -> ServerConfig:
|
|
62
|
+
base = config_from_env(surface, api_key=args.api_key)
|
|
63
|
+
allowed_origins = tuple(args.allowed_origin) or parse_csv(None)
|
|
64
|
+
transport = normalise_transport(args.transport) if args.transport else base.transport
|
|
65
|
+
return ServerConfig(
|
|
66
|
+
surface=surface,
|
|
67
|
+
api_key=base.api_key,
|
|
68
|
+
app_base_url=args.app_base_url or base.app_base_url,
|
|
69
|
+
sending_base_url=args.sending_base_url or base.sending_base_url,
|
|
70
|
+
transport=transport,
|
|
71
|
+
host=args.host or base.host,
|
|
72
|
+
port=args.port or base.port,
|
|
73
|
+
path=args.path or base.path,
|
|
74
|
+
openapi_input_dir=args.openapi_input_dir or base.openapi_input_dir,
|
|
75
|
+
app_openapi=args.app_openapi or base.app_openapi,
|
|
76
|
+
sending_openapi=args.sending_openapi or base.sending_openapi,
|
|
77
|
+
allowed_origins=allowed_origins or base.allowed_origins,
|
|
78
|
+
http_bearer_token=args.http_bearer_token or base.http_bearer_token,
|
|
79
|
+
allow_unauthenticated_http=args.allow_unauthenticated_http or base.allow_unauthenticated_http,
|
|
80
|
+
timeout_seconds=args.timeout_seconds or base.timeout_seconds,
|
|
81
|
+
stateless_http=base.stateless_http,
|
|
82
|
+
retry=RetryConfig(
|
|
83
|
+
max_attempts=args.retry_max_attempts or base.retry.max_attempts,
|
|
84
|
+
base_delay_seconds=args.retry_base_delay_seconds or base.retry.base_delay_seconds,
|
|
85
|
+
max_delay_seconds=args.retry_max_delay_seconds or base.retry.max_delay_seconds,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from sendmux_core import validate_api_key
|
|
8
|
+
|
|
9
|
+
Surface = Literal["mailbox", "management", "sending"]
|
|
10
|
+
Transport = Literal["stdio", "http", "streamable-http"]
|
|
11
|
+
|
|
12
|
+
DEFAULT_APP_BASE_URL = "https://app.sendmux.ai/api/v1"
|
|
13
|
+
DEFAULT_SENDING_BASE_URL = "https://smtp.sendmux.ai/api/v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class RetryConfig:
|
|
18
|
+
max_attempts: int = 3
|
|
19
|
+
base_delay_seconds: float = 0.25
|
|
20
|
+
max_delay_seconds: float = 8.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ServerConfig:
|
|
25
|
+
surface: Surface
|
|
26
|
+
api_key: str
|
|
27
|
+
app_base_url: str = DEFAULT_APP_BASE_URL
|
|
28
|
+
sending_base_url: str = DEFAULT_SENDING_BASE_URL
|
|
29
|
+
transport: Transport = "stdio"
|
|
30
|
+
host: str = "127.0.0.1"
|
|
31
|
+
port: int = 8765
|
|
32
|
+
path: str = "/mcp"
|
|
33
|
+
openapi_input_dir: str | None = None
|
|
34
|
+
app_openapi: str | None = None
|
|
35
|
+
sending_openapi: str | None = None
|
|
36
|
+
allowed_origins: tuple[str, ...] = ()
|
|
37
|
+
http_bearer_token: str | None = None
|
|
38
|
+
allow_unauthenticated_http: bool = False
|
|
39
|
+
timeout_seconds: float = 30.0
|
|
40
|
+
stateless_http: bool = True
|
|
41
|
+
retry: RetryConfig = RetryConfig()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def required_key_surface(self) -> Literal["root", "mailbox"]:
|
|
45
|
+
return "mailbox" if self.surface == "mailbox" else "root"
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def api_base_url(self) -> str:
|
|
49
|
+
return self.sending_base_url if self.surface == "sending" else self.app_base_url
|
|
50
|
+
|
|
51
|
+
def validate(self) -> None:
|
|
52
|
+
validate_api_key(self.api_key, surface=self.required_key_surface)
|
|
53
|
+
if self.transport in {"http", "streamable-http"} and not self.allow_unauthenticated_http:
|
|
54
|
+
if not self.http_bearer_token:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"HTTP transport requires SENDMUX_MCP_HTTP_BEARER_TOKEN or --allow-unauthenticated-http."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def config_from_env(surface: Surface, *, api_key: str | None = None) -> ServerConfig:
|
|
61
|
+
transport = normalise_transport(os.environ.get("SENDMUX_MCP_TRANSPORT", "stdio"))
|
|
62
|
+
return ServerConfig(
|
|
63
|
+
surface=surface,
|
|
64
|
+
api_key=api_key or require_env("SENDMUX_API_KEY"),
|
|
65
|
+
app_base_url=os.environ.get("SENDMUX_APP_BASE_URL", DEFAULT_APP_BASE_URL),
|
|
66
|
+
sending_base_url=os.environ.get("SENDMUX_SENDING_BASE_URL", DEFAULT_SENDING_BASE_URL),
|
|
67
|
+
transport=transport,
|
|
68
|
+
host=os.environ.get("SENDMUX_MCP_HOST", "127.0.0.1"),
|
|
69
|
+
port=int(os.environ.get("SENDMUX_MCP_PORT", "8765")),
|
|
70
|
+
path=os.environ.get("SENDMUX_MCP_PATH", "/mcp"),
|
|
71
|
+
openapi_input_dir=os.environ.get("SENDMUX_MCP_OPENAPI_INPUT_DIR") or os.environ.get("OPENAPI_INPUT_DIR"),
|
|
72
|
+
app_openapi=os.environ.get("SENDMUX_MCP_APP_OPENAPI"),
|
|
73
|
+
sending_openapi=os.environ.get("SENDMUX_MCP_SENDING_OPENAPI"),
|
|
74
|
+
allowed_origins=parse_csv(os.environ.get("SENDMUX_MCP_ALLOWED_ORIGINS")),
|
|
75
|
+
http_bearer_token=os.environ.get("SENDMUX_MCP_HTTP_BEARER_TOKEN"),
|
|
76
|
+
allow_unauthenticated_http=parse_bool(os.environ.get("SENDMUX_MCP_ALLOW_UNAUTHENTICATED_HTTP")),
|
|
77
|
+
timeout_seconds=float(os.environ.get("SENDMUX_MCP_TIMEOUT_SECONDS", "30")),
|
|
78
|
+
stateless_http=parse_bool(os.environ.get("SENDMUX_MCP_STATELESS_HTTP"), default=True),
|
|
79
|
+
retry=RetryConfig(
|
|
80
|
+
max_attempts=int(os.environ.get("SENDMUX_MCP_RETRY_MAX_ATTEMPTS", "3")),
|
|
81
|
+
base_delay_seconds=float(os.environ.get("SENDMUX_MCP_RETRY_BASE_DELAY_SECONDS", "0.25")),
|
|
82
|
+
max_delay_seconds=float(os.environ.get("SENDMUX_MCP_RETRY_MAX_DELAY_SECONDS", "8")),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def normalise_transport(value: str) -> Transport:
|
|
88
|
+
if value == "streamable-http":
|
|
89
|
+
return "streamable-http"
|
|
90
|
+
if value in {"stdio", "http"}:
|
|
91
|
+
return value # type: ignore[return-value]
|
|
92
|
+
raise ValueError("transport must be one of: stdio, http, streamable-http")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_csv(value: str | None) -> tuple[str, ...]:
|
|
96
|
+
if not value:
|
|
97
|
+
return ()
|
|
98
|
+
return tuple(item.strip() for item in value.split(",") if item.strip())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_bool(value: str | None, *, default: bool = False) -> bool:
|
|
102
|
+
if value is None:
|
|
103
|
+
return default
|
|
104
|
+
return value.lower() in {"1", "true", "yes", "on"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def require_env(name: str) -> str:
|
|
108
|
+
value = os.environ.get(name)
|
|
109
|
+
if not value:
|
|
110
|
+
raise ValueError(f"Missing required environment variable {name}")
|
|
111
|
+
return value
|