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.
@@ -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,6 @@
1
+ from __future__ import annotations
2
+
3
+ from sendmux_mcp.config import ServerConfig, Surface
4
+ from sendmux_mcp.server import create_server
5
+
6
+ __all__ = ["ServerConfig", "Surface", "create_server"]
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from sendmux_mcp.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -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