mammoth-mcp-server 0.1.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.
- mammoth_mcp_server-0.1.0/.gitignore +72 -0
- mammoth_mcp_server-0.1.0/PKG-INFO +166 -0
- mammoth_mcp_server-0.1.0/README.md +148 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/__init__.py +3 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/app.py +55 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/config.py +53 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/consts.py +442 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/deadline.py +29 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/jobs.py +123 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/sdk.py +196 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/server.py +47 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/store.py +75 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/task_params.py +735 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tokens.py +49 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tool_kinds.py +15 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/__init__.py +27 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/automations.py +266 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/dashboards.py +328 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/data.py +59 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/discovery.py +173 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/exports.py +227 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/files.py +668 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/help_topics.py +69 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/pipeline.py +368 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/transformations.py +142 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/tools/views.py +90 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/transform_operations.py +1224 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/upload_app.py +266 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/upload_report.py +362 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/upload_routes.py +222 -0
- mammoth_mcp_server-0.1.0/mammoth_mcp_server/upload_tickets.py +122 -0
- mammoth_mcp_server-0.1.0/pyproject.toml +56 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Bytecode
|
|
2
|
+
*.pyc
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
|
|
6
|
+
# Environments
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
env/
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
|
|
16
|
+
# Testing / coverage
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
.tox/
|
|
20
|
+
|
|
21
|
+
# Jupyter
|
|
22
|
+
.ipynb_checkpoints/
|
|
23
|
+
|
|
24
|
+
# IDEs
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
|
|
28
|
+
# Type checker
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
.pytype/
|
|
31
|
+
.pyre/
|
|
32
|
+
|
|
33
|
+
# Linting / tools
|
|
34
|
+
.ruff_cache/
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
|
|
37
|
+
# Build artifacts
|
|
38
|
+
*.whl
|
|
39
|
+
*.egg
|
|
40
|
+
*.egg-link
|
|
41
|
+
|
|
42
|
+
# MCP server local config
|
|
43
|
+
mammoth-mcp/.venv/
|
|
44
|
+
mammoth-mcp/poetry.toml
|
|
45
|
+
|
|
46
|
+
# Stale dev / scratch files
|
|
47
|
+
test_debug.py
|
|
48
|
+
test_live_api.py
|
|
49
|
+
stress.py
|
|
50
|
+
employee.csv
|
|
51
|
+
Store_Transactions.csv
|
|
52
|
+
openapi.json
|
|
53
|
+
# The CLI's pinned OpenAPI snapshot is a committed source artifact: contract
|
|
54
|
+
# tests read it and the built wheel ships it. Keep it tracked despite the
|
|
55
|
+
# broad rule above (which still ignores stray downloads elsewhere).
|
|
56
|
+
!mammoth-cli/spec/openapi/openapi.json
|
|
57
|
+
|
|
58
|
+
# Env files
|
|
59
|
+
.env
|
|
60
|
+
*.env
|
|
61
|
+
bridge/.env
|
|
62
|
+
|
|
63
|
+
# Local-only reference / build output (not part of the SDK)
|
|
64
|
+
mvc-service/
|
|
65
|
+
site/
|
|
66
|
+
bridge/
|
|
67
|
+
*.docx
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Transient STE text corpus
|
|
71
|
+
mammoth-cli/build/
|
|
72
|
+
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mammoth-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Mammoth Analytics: an AI client explores, transforms and exports data as the signed-in user
|
|
5
|
+
Project-URL: homepage, https://mammoth.io
|
|
6
|
+
Project-URL: repository, https://github.com/EdgeMetric/mammothsdk
|
|
7
|
+
Author-email: Mammoth Analytics <support@mammoth.io>
|
|
8
|
+
License-Expression: LicenseRef-Proprietary
|
|
9
|
+
Requires-Python: <3.15,>=3.12
|
|
10
|
+
Requires-Dist: cryptography<50,>=48.0.1
|
|
11
|
+
Requires-Dist: mammoth-io<0.9,>=0.8.25
|
|
12
|
+
Requires-Dist: mcp<3,>=2.2.0
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.12.5
|
|
14
|
+
Requires-Dist: python-multipart>=0.0.18
|
|
15
|
+
Requires-Dist: redis<8,>=5
|
|
16
|
+
Requires-Dist: uvicorn>=0.34
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# mammoth-mcp-server
|
|
20
|
+
|
|
21
|
+
The Mammoth MCP server. An AI client (Claude, ChatGPT) uses its tools to explore, clean, transform
|
|
22
|
+
and export data in Mammoth, as the signed-in user.
|
|
23
|
+
|
|
24
|
+
The server depends on one Mammoth package: the SDK (`mammoth-io`). Every tool reaches Mammoth
|
|
25
|
+
through the API, as the SDK does. The server imports no backend code.
|
|
26
|
+
|
|
27
|
+
## Layout
|
|
28
|
+
|
|
29
|
+
| Path | What it holds |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `mammoth_mcp_server/tools/` | The tools, one module per area (views, pipeline, files, dashboards, ...). |
|
|
32
|
+
| `mammoth_mcp_server/sdk.py` | The SDK client a tool call is given, and the API's errors in words a model can use. |
|
|
33
|
+
| `mammoth_mcp_server/jobs.py` | The wait for a job that an API route started. |
|
|
34
|
+
| `mammoth_mcp_server/task_params.py` | The map from a transformation to the pipeline task the API takes. |
|
|
35
|
+
| `mammoth_mcp_server/tokens.py` | The check of a caller's token: Mammoth says whether it is live and which server it is for. |
|
|
36
|
+
| `mammoth_mcp_server/store.py` | Short-lived records in Redis: upload tickets. |
|
|
37
|
+
| `mammoth_mcp_server/upload_routes.py` | The page a user drops a local file on. |
|
|
38
|
+
| `mammoth_mcp_server/app.py` | The ASGI app and `main()`. |
|
|
39
|
+
| `mammoth_mcp_server/config.py` | Every deployment setting, read from the environment. |
|
|
40
|
+
|
|
41
|
+
## Settings
|
|
42
|
+
|
|
43
|
+
| Variable | Default | Meaning |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `MCP_SERVER_URL` | `https://mcp.mammoth.io` | This server, as a client reaches it. |
|
|
46
|
+
| `MAMMOTH_APP_URL` | `https://app.mammoth.io` | The web app, for the links a tool gives the user. |
|
|
47
|
+
| `MAMMOTH_DASHBOARD_URL` | `MAMMOTH_APP_URL` | The host that serves a published dashboard. |
|
|
48
|
+
| `MAMMOTH_API_URL` | `MAMMOTH_APP_URL` + `/api/v2` | The API every tool call goes to. |
|
|
49
|
+
| `MAMMOTH_API_ROOT` | `/api/v2` | The path the API is served under. Set it empty for an API reached directly. |
|
|
50
|
+
| `MAMMOTH_OAUTH_URL` | `MAMMOTH_APP_URL` + `/api/v2` | Mammoth's authorization server (its issuer), where a client signs the user in. |
|
|
51
|
+
| `MCP_REDIS_URL` | `redis://localhost:6379/0` | Where upload tickets are kept. |
|
|
52
|
+
| `MCP_ENCRYPTION_KEY` | none | A Fernet key. The caller's token in an upload ticket is sealed with it. Required. |
|
|
53
|
+
| `MCP_STORE_PREFIX` | `mammoth_mcp` | Keeps this deployment's records apart in a shared Redis. |
|
|
54
|
+
| `MCP_HOST`, `MCP_PORT` | `127.0.0.1`, `8270` | Where the server listens. |
|
|
55
|
+
|
|
56
|
+
## Sign-in
|
|
57
|
+
|
|
58
|
+
This server signs nobody in. Mammoth's own authorization server does: the OAuth server the Mammoth
|
|
59
|
+
API serves. This server is an OAuth resource server only.
|
|
60
|
+
|
|
61
|
+
1. A client calls `/mcp` with no token. The server answers `401` and points to
|
|
62
|
+
`/.well-known/oauth-protected-resource/mcp`.
|
|
63
|
+
2. That document names Mammoth's authorization server (`MAMMOTH_OAUTH_URL`). The client reads the
|
|
64
|
+
server's metadata there and sends the user to it.
|
|
65
|
+
3. The user signs in to Mammoth, picks a workspace and a project, and approves. The client asks
|
|
66
|
+
for a token for this server's URI, `<MCP_SERVER_URL>/mcp` (the `resource` parameter, RFC 8707).
|
|
67
|
+
4. The client calls `/mcp` with that token. On every request the server asks Mammoth what the token
|
|
68
|
+
is for (`GET /workspaces/current`). The server takes the token only when Mammoth says it is live
|
|
69
|
+
and was issued for this server's URI.
|
|
70
|
+
5. Each tool call goes to the Mammoth API with the same token, so the user's permissions apply.
|
|
71
|
+
|
|
72
|
+
A token belongs to one workspace. A tool call for another workspace is refused. A Mammoth API token
|
|
73
|
+
made by hand in Workspace settings was issued for no server, so this server refuses it.
|
|
74
|
+
|
|
75
|
+
The server keeps no sign-in and no token, with one exception: an upload link holds the caller's
|
|
76
|
+
token, sealed, in Redis until the user uploads or 30 minutes pass.
|
|
77
|
+
|
|
78
|
+
### What Mammoth needs
|
|
79
|
+
|
|
80
|
+
- The MCP server's URI, `<MCP_SERVER_URL>/mcp`, in mvc-service `const.ini`, section `[oauth]`,
|
|
81
|
+
key `MCP_RESOURCES`. Mammoth issues a token for no other resource.
|
|
82
|
+
- `GET /workspaces/current` must return the token's `resource` beside the workspace `id`. Without
|
|
83
|
+
it this server refuses every token.
|
|
84
|
+
|
|
85
|
+
**Before it works in an environment** below lists every step, in the order to check them.
|
|
86
|
+
|
|
87
|
+
## Run
|
|
88
|
+
|
|
89
|
+
### As a container, which needs nothing on the host but Docker
|
|
90
|
+
|
|
91
|
+
The image is built from this repository, so a deployment never waits for a
|
|
92
|
+
release of the SDK. Build it from the **repository root**, which is the build
|
|
93
|
+
context:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cp mammoth-mcp-server/deploy/.dockerignore.example .dockerignore
|
|
97
|
+
docker build -f mammoth-mcp-server/deploy/Dockerfile -t mammoth-mcp-server .
|
|
98
|
+
docker run --env-file mammoth-mcp-server/deploy/.env -p 8270:8270 mammoth-mcp-server
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With a Redis of its own, as one stack:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cp mammoth-mcp-server/.env.example mammoth-mcp-server/deploy/.env # then set the values
|
|
105
|
+
docker compose -f mammoth-mcp-server/deploy/compose.yaml up -d --build
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The server keeps nothing in memory between requests, so it scales by adding
|
|
109
|
+
containers. They share only Redis.
|
|
110
|
+
|
|
111
|
+
### From a virtual environment
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install mammoth-mcp-server
|
|
115
|
+
cp .env.example .env # then set the values
|
|
116
|
+
mammoth-mcp-server # or: uvicorn mammoth_mcp_server.app:create_app --factory
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### What `deploy/` holds
|
|
120
|
+
|
|
121
|
+
| File | What it is for |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `Dockerfile` | The image, built from this repository |
|
|
124
|
+
| `compose.yaml` | The server and a Redis, as one stack |
|
|
125
|
+
| `.dockerignore.example` | Copy to the repository root before building |
|
|
126
|
+
| `mammoth-mcp-server-container.service.example` | systemd, running the container |
|
|
127
|
+
| `mammoth-mcp-server.service.example` | systemd, running a virtual environment |
|
|
128
|
+
| `nginx.example.conf` | Both server blocks a deployment needs |
|
|
129
|
+
|
|
130
|
+
## Before it works in an environment
|
|
131
|
+
|
|
132
|
+
Each of these stops the sign-in on its own, so check them in order.
|
|
133
|
+
|
|
134
|
+
1. **DNS and the front door** point `MCP_SERVER_URL` at the server's port.
|
|
135
|
+
`deploy/nginx.example.conf` holds the server block.
|
|
136
|
+
2. **`MCP_ENCRYPTION_KEY` is set.** It is the only secret this server keeps.
|
|
137
|
+
Nothing else is required that has no default; see **Settings**.
|
|
138
|
+
3. **Mammoth knows this server:** `MCP_RESOURCES = <MCP_SERVER_URL>/mcp`, in
|
|
139
|
+
mvc-service `const.ini`, section `[oauth]`. Mammoth issues a token for no
|
|
140
|
+
other resource.
|
|
141
|
+
4. **The Mammoth host serves its OAuth metadata where a client looks for it.**
|
|
142
|
+
For an issuer with a path, RFC 8414 puts the document at
|
|
143
|
+
`/.well-known/oauth-authorization-server/api/v2`, and apiv2 serves it at its
|
|
144
|
+
own path, so the front door has to join the two. The second server block in
|
|
145
|
+
`deploy/nginx.example.conf` does it. Check it with:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
curl https://<mammoth host>/.well-known/oauth-authorization-server/api/v2
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It must answer the document, not `404`.
|
|
152
|
+
5. **The web app's OAuth consent page is deployed.** Without it the user lands
|
|
153
|
+
on a page that does not exist, and approves nothing.
|
|
154
|
+
|
|
155
|
+
Then a client connects to `<MCP_SERVER_URL>/mcp`, and the user signs in to
|
|
156
|
+
Mammoth and picks a workspace and a project.
|
|
157
|
+
|
|
158
|
+
## Tests
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cd mammoth-mcp-server
|
|
162
|
+
python -m pytest tests -q
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The tests reach no server. The SDK client and the tools are real; only the HTTP transport under
|
|
166
|
+
the client is replaced.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# mammoth-mcp-server
|
|
2
|
+
|
|
3
|
+
The Mammoth MCP server. An AI client (Claude, ChatGPT) uses its tools to explore, clean, transform
|
|
4
|
+
and export data in Mammoth, as the signed-in user.
|
|
5
|
+
|
|
6
|
+
The server depends on one Mammoth package: the SDK (`mammoth-io`). Every tool reaches Mammoth
|
|
7
|
+
through the API, as the SDK does. The server imports no backend code.
|
|
8
|
+
|
|
9
|
+
## Layout
|
|
10
|
+
|
|
11
|
+
| Path | What it holds |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `mammoth_mcp_server/tools/` | The tools, one module per area (views, pipeline, files, dashboards, ...). |
|
|
14
|
+
| `mammoth_mcp_server/sdk.py` | The SDK client a tool call is given, and the API's errors in words a model can use. |
|
|
15
|
+
| `mammoth_mcp_server/jobs.py` | The wait for a job that an API route started. |
|
|
16
|
+
| `mammoth_mcp_server/task_params.py` | The map from a transformation to the pipeline task the API takes. |
|
|
17
|
+
| `mammoth_mcp_server/tokens.py` | The check of a caller's token: Mammoth says whether it is live and which server it is for. |
|
|
18
|
+
| `mammoth_mcp_server/store.py` | Short-lived records in Redis: upload tickets. |
|
|
19
|
+
| `mammoth_mcp_server/upload_routes.py` | The page a user drops a local file on. |
|
|
20
|
+
| `mammoth_mcp_server/app.py` | The ASGI app and `main()`. |
|
|
21
|
+
| `mammoth_mcp_server/config.py` | Every deployment setting, read from the environment. |
|
|
22
|
+
|
|
23
|
+
## Settings
|
|
24
|
+
|
|
25
|
+
| Variable | Default | Meaning |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `MCP_SERVER_URL` | `https://mcp.mammoth.io` | This server, as a client reaches it. |
|
|
28
|
+
| `MAMMOTH_APP_URL` | `https://app.mammoth.io` | The web app, for the links a tool gives the user. |
|
|
29
|
+
| `MAMMOTH_DASHBOARD_URL` | `MAMMOTH_APP_URL` | The host that serves a published dashboard. |
|
|
30
|
+
| `MAMMOTH_API_URL` | `MAMMOTH_APP_URL` + `/api/v2` | The API every tool call goes to. |
|
|
31
|
+
| `MAMMOTH_API_ROOT` | `/api/v2` | The path the API is served under. Set it empty for an API reached directly. |
|
|
32
|
+
| `MAMMOTH_OAUTH_URL` | `MAMMOTH_APP_URL` + `/api/v2` | Mammoth's authorization server (its issuer), where a client signs the user in. |
|
|
33
|
+
| `MCP_REDIS_URL` | `redis://localhost:6379/0` | Where upload tickets are kept. |
|
|
34
|
+
| `MCP_ENCRYPTION_KEY` | none | A Fernet key. The caller's token in an upload ticket is sealed with it. Required. |
|
|
35
|
+
| `MCP_STORE_PREFIX` | `mammoth_mcp` | Keeps this deployment's records apart in a shared Redis. |
|
|
36
|
+
| `MCP_HOST`, `MCP_PORT` | `127.0.0.1`, `8270` | Where the server listens. |
|
|
37
|
+
|
|
38
|
+
## Sign-in
|
|
39
|
+
|
|
40
|
+
This server signs nobody in. Mammoth's own authorization server does: the OAuth server the Mammoth
|
|
41
|
+
API serves. This server is an OAuth resource server only.
|
|
42
|
+
|
|
43
|
+
1. A client calls `/mcp` with no token. The server answers `401` and points to
|
|
44
|
+
`/.well-known/oauth-protected-resource/mcp`.
|
|
45
|
+
2. That document names Mammoth's authorization server (`MAMMOTH_OAUTH_URL`). The client reads the
|
|
46
|
+
server's metadata there and sends the user to it.
|
|
47
|
+
3. The user signs in to Mammoth, picks a workspace and a project, and approves. The client asks
|
|
48
|
+
for a token for this server's URI, `<MCP_SERVER_URL>/mcp` (the `resource` parameter, RFC 8707).
|
|
49
|
+
4. The client calls `/mcp` with that token. On every request the server asks Mammoth what the token
|
|
50
|
+
is for (`GET /workspaces/current`). The server takes the token only when Mammoth says it is live
|
|
51
|
+
and was issued for this server's URI.
|
|
52
|
+
5. Each tool call goes to the Mammoth API with the same token, so the user's permissions apply.
|
|
53
|
+
|
|
54
|
+
A token belongs to one workspace. A tool call for another workspace is refused. A Mammoth API token
|
|
55
|
+
made by hand in Workspace settings was issued for no server, so this server refuses it.
|
|
56
|
+
|
|
57
|
+
The server keeps no sign-in and no token, with one exception: an upload link holds the caller's
|
|
58
|
+
token, sealed, in Redis until the user uploads or 30 minutes pass.
|
|
59
|
+
|
|
60
|
+
### What Mammoth needs
|
|
61
|
+
|
|
62
|
+
- The MCP server's URI, `<MCP_SERVER_URL>/mcp`, in mvc-service `const.ini`, section `[oauth]`,
|
|
63
|
+
key `MCP_RESOURCES`. Mammoth issues a token for no other resource.
|
|
64
|
+
- `GET /workspaces/current` must return the token's `resource` beside the workspace `id`. Without
|
|
65
|
+
it this server refuses every token.
|
|
66
|
+
|
|
67
|
+
**Before it works in an environment** below lists every step, in the order to check them.
|
|
68
|
+
|
|
69
|
+
## Run
|
|
70
|
+
|
|
71
|
+
### As a container, which needs nothing on the host but Docker
|
|
72
|
+
|
|
73
|
+
The image is built from this repository, so a deployment never waits for a
|
|
74
|
+
release of the SDK. Build it from the **repository root**, which is the build
|
|
75
|
+
context:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cp mammoth-mcp-server/deploy/.dockerignore.example .dockerignore
|
|
79
|
+
docker build -f mammoth-mcp-server/deploy/Dockerfile -t mammoth-mcp-server .
|
|
80
|
+
docker run --env-file mammoth-mcp-server/deploy/.env -p 8270:8270 mammoth-mcp-server
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
With a Redis of its own, as one stack:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cp mammoth-mcp-server/.env.example mammoth-mcp-server/deploy/.env # then set the values
|
|
87
|
+
docker compose -f mammoth-mcp-server/deploy/compose.yaml up -d --build
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The server keeps nothing in memory between requests, so it scales by adding
|
|
91
|
+
containers. They share only Redis.
|
|
92
|
+
|
|
93
|
+
### From a virtual environment
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install mammoth-mcp-server
|
|
97
|
+
cp .env.example .env # then set the values
|
|
98
|
+
mammoth-mcp-server # or: uvicorn mammoth_mcp_server.app:create_app --factory
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### What `deploy/` holds
|
|
102
|
+
|
|
103
|
+
| File | What it is for |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `Dockerfile` | The image, built from this repository |
|
|
106
|
+
| `compose.yaml` | The server and a Redis, as one stack |
|
|
107
|
+
| `.dockerignore.example` | Copy to the repository root before building |
|
|
108
|
+
| `mammoth-mcp-server-container.service.example` | systemd, running the container |
|
|
109
|
+
| `mammoth-mcp-server.service.example` | systemd, running a virtual environment |
|
|
110
|
+
| `nginx.example.conf` | Both server blocks a deployment needs |
|
|
111
|
+
|
|
112
|
+
## Before it works in an environment
|
|
113
|
+
|
|
114
|
+
Each of these stops the sign-in on its own, so check them in order.
|
|
115
|
+
|
|
116
|
+
1. **DNS and the front door** point `MCP_SERVER_URL` at the server's port.
|
|
117
|
+
`deploy/nginx.example.conf` holds the server block.
|
|
118
|
+
2. **`MCP_ENCRYPTION_KEY` is set.** It is the only secret this server keeps.
|
|
119
|
+
Nothing else is required that has no default; see **Settings**.
|
|
120
|
+
3. **Mammoth knows this server:** `MCP_RESOURCES = <MCP_SERVER_URL>/mcp`, in
|
|
121
|
+
mvc-service `const.ini`, section `[oauth]`. Mammoth issues a token for no
|
|
122
|
+
other resource.
|
|
123
|
+
4. **The Mammoth host serves its OAuth metadata where a client looks for it.**
|
|
124
|
+
For an issuer with a path, RFC 8414 puts the document at
|
|
125
|
+
`/.well-known/oauth-authorization-server/api/v2`, and apiv2 serves it at its
|
|
126
|
+
own path, so the front door has to join the two. The second server block in
|
|
127
|
+
`deploy/nginx.example.conf` does it. Check it with:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
curl https://<mammoth host>/.well-known/oauth-authorization-server/api/v2
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
It must answer the document, not `404`.
|
|
134
|
+
5. **The web app's OAuth consent page is deployed.** Without it the user lands
|
|
135
|
+
on a page that does not exist, and approves nothing.
|
|
136
|
+
|
|
137
|
+
Then a client connects to `<MCP_SERVER_URL>/mcp`, and the user signs in to
|
|
138
|
+
Mammoth and picks a workspace and a project.
|
|
139
|
+
|
|
140
|
+
## Tests
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
cd mammoth-mcp-server
|
|
144
|
+
python -m pytest tests -q
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The tests reach no server. The SDK client and the tools are real; only the HTTP transport under
|
|
148
|
+
the client is replaced.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The MCP server as a process of its own: the ASGI app, and how to run it."""
|
|
2
|
+
|
|
3
|
+
import uvicorn
|
|
4
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
5
|
+
from starlette.applications import Starlette
|
|
6
|
+
|
|
7
|
+
from .config import HOST, MCP_PATH, PORT
|
|
8
|
+
from .server import mcp_server, register_tools
|
|
9
|
+
from .store import cipher
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_settings() -> None:
|
|
13
|
+
"""Fail now, with the reason, rather than at the first upload.
|
|
14
|
+
|
|
15
|
+
A worker that dies at startup is restarted for ever by the process that
|
|
16
|
+
runs it, so the deployment looks alive and serves nothing. Run this before
|
|
17
|
+
the server starts, and the process stops with the reason instead.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
RuntimeError: If the deployment set no `MCP_ENCRYPTION_KEY`.
|
|
21
|
+
ValueError: If it set one that is not a Fernet key.
|
|
22
|
+
"""
|
|
23
|
+
cipher()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_app() -> Starlette:
|
|
27
|
+
"""Build the app a server runs: every tool, behind the token check.
|
|
28
|
+
|
|
29
|
+
Stateless JSON mode: any worker can serve any request, and no response is
|
|
30
|
+
a stream. So the server scales by adding workers, with nothing shared
|
|
31
|
+
between them but the store.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
RuntimeError: If the deployment set no `MCP_ENCRYPTION_KEY`.
|
|
35
|
+
ValueError: If it set one that is not a Fernet key.
|
|
36
|
+
"""
|
|
37
|
+
check_settings()
|
|
38
|
+
register_tools()
|
|
39
|
+
return mcp_server.streamable_http_app(
|
|
40
|
+
streamable_http_path=MCP_PATH,
|
|
41
|
+
stateless_http=True,
|
|
42
|
+
json_response=True,
|
|
43
|
+
# DNS-rebinding protection is for servers on localhost. Here a browser on
|
|
44
|
+
# another origin cannot send the bearer token: CORS preflight stops it.
|
|
45
|
+
transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
"""Run the server on the host and port the environment names."""
|
|
51
|
+
uvicorn.run(create_app(), host=HOST, port=PORT)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""What this deployment is: its URLs, the Mammoth API it calls, and its store.
|
|
2
|
+
|
|
3
|
+
Every value comes from the environment, so one build runs anywhere. The server
|
|
4
|
+
names no Mammoth code but the SDK: it reaches Mammoth through the API alone.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read(name: str, default: str) -> str:
|
|
12
|
+
"""One setting from the environment, or its default when it is not set."""
|
|
13
|
+
return os.environ.get(name) or default
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# The web app, as a user's browser reaches it.
|
|
17
|
+
APP_URL = read("MAMMOTH_APP_URL", "https://app.mammoth.io").rstrip("/")
|
|
18
|
+
# The host that serves a published dashboard.
|
|
19
|
+
DASHBOARD_DOMAIN = read("MAMMOTH_DASHBOARD_URL", APP_URL).rstrip("/")
|
|
20
|
+
|
|
21
|
+
# The Mammoth API every tool call is sent to, as the SDK takes it.
|
|
22
|
+
API_URL = read("MAMMOTH_API_URL", f"{APP_URL}/api/v2").rstrip("/")
|
|
23
|
+
# The path the API is served under. Empty for a server that is reached
|
|
24
|
+
# directly, which mounts its routes at their own paths.
|
|
25
|
+
API_ROOT: str | None = os.environ.get("MAMMOTH_API_ROOT", "/api/v2") or None
|
|
26
|
+
|
|
27
|
+
# This server, as a client reaches it. The canonical MCP server URI (RFC 8707)
|
|
28
|
+
# is this URL plus `/mcp`: a client asks for a token for it.
|
|
29
|
+
SERVER_URL = read("MCP_SERVER_URL", "https://mcp.mammoth.io").rstrip("/")
|
|
30
|
+
MCP_PATH = "/mcp"
|
|
31
|
+
MCP_RESOURCE_URL = f"{SERVER_URL}{MCP_PATH}"
|
|
32
|
+
# The authorization server that signs users in for this server: Mammoth's own,
|
|
33
|
+
# which the API serves. A client is sent there, and a token it issues for
|
|
34
|
+
# `MCP_RESOURCE_URL` is the one bearer this server takes.
|
|
35
|
+
MCP_OAUTH_URL = read("MAMMOTH_OAUTH_URL", f"{APP_URL}/api/v2").rstrip("/")
|
|
36
|
+
# The upload page, as the user's browser reaches it. A tool hands out this URL
|
|
37
|
+
# with a ticket.
|
|
38
|
+
MCP_UPLOAD_PATH = "/upload"
|
|
39
|
+
MCP_UPLOAD_URL = f"{SERVER_URL}{MCP_UPLOAD_PATH}"
|
|
40
|
+
# The origin of the upload page: the host lets the in-chat uploader's frame
|
|
41
|
+
# fetch only from the origins its resource names.
|
|
42
|
+
UPLOAD_ORIGIN = "{0.scheme}://{0.netloc}".format(urlparse(MCP_UPLOAD_URL))
|
|
43
|
+
|
|
44
|
+
# Where upload tickets are kept, and the key the caller's token in one is
|
|
45
|
+
# sealed with. Generate the key with
|
|
46
|
+
# `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`.
|
|
47
|
+
REDIS_URL = read("MCP_REDIS_URL", "redis://localhost:6379/0")
|
|
48
|
+
ENCRYPTION_KEY = read("MCP_ENCRYPTION_KEY", "")
|
|
49
|
+
# Keeps this server's records apart from another deployment's in one Redis.
|
|
50
|
+
STORE_PREFIX = read("MCP_STORE_PREFIX", "mammoth_mcp")
|
|
51
|
+
|
|
52
|
+
HOST = read("MCP_HOST", "127.0.0.1")
|
|
53
|
+
PORT = int(read("MCP_PORT", "8270"))
|