redhat-datalayer-mcp 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.
- redhat_datalayer_mcp-0.1.0/.gitignore +12 -0
- redhat_datalayer_mcp-0.1.0/CONTEXT.md +23 -0
- redhat_datalayer_mcp-0.1.0/PKG-INFO +563 -0
- redhat_datalayer_mcp-0.1.0/README.md +547 -0
- redhat_datalayer_mcp-0.1.0/docs/adr/0001-datalayer-extension-as-lifespan-not-provider.md +19 -0
- redhat_datalayer_mcp-0.1.0/docs/adr/0002-filesystem-provider-with-ast-collision-detection.md +19 -0
- redhat_datalayer_mcp-0.1.0/pyproject.toml +42 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/__init__.py +27 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/config.py +70 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/env.py +18 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/errors.py +53 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/extension.py +119 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/parse.py +93 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/py.typed +0 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/runtime.py +84 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/scan.py +77 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/schema.py +60 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/server.py +26 -0
- redhat_datalayer_mcp-0.1.0/src/datalayer_mcp/tools.py +75 -0
- redhat_datalayer_mcp-0.1.0/tests/conftest.py +105 -0
- redhat_datalayer_mcp-0.1.0/tests/fixtures/operations/Cves.graphql +12 -0
- redhat_datalayer_mcp-0.1.0/tests/test_config.py +67 -0
- redhat_datalayer_mcp-0.1.0/tests/test_env.py +35 -0
- redhat_datalayer_mcp-0.1.0/tests/test_errors.py +31 -0
- redhat_datalayer_mcp-0.1.0/tests/test_extension.py +216 -0
- redhat_datalayer_mcp-0.1.0/tests/test_parse.py +156 -0
- redhat_datalayer_mcp-0.1.0/tests/test_registry.py +24 -0
- redhat_datalayer_mcp-0.1.0/tests/test_runtime.py +71 -0
- redhat_datalayer_mcp-0.1.0/tests/test_schema.py +17 -0
- redhat_datalayer_mcp-0.1.0/tests/test_server.py +123 -0
- redhat_datalayer_mcp-0.1.0/tests/test_tools.py +27 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# datalayer-mcp
|
|
2
|
+
|
|
3
|
+
MCP server factory that turns curated `.graphql` operation files and user-defined tools into a FastMCP server backed by the Red Hat supergraph.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**Operation**: A named GraphQL query or mutation defined in a single `.graphql` file. Becomes one auto-tool on the MCP server. One operation per file; duplicate names across files are a hard startup error.
|
|
8
|
+
_Avoid_: query, request, command
|
|
9
|
+
|
|
10
|
+
**Auto-tool**: An MCP tool generated automatically from an operation file. Executes the raw GraphQL operation against the supergraph. No user code required.
|
|
11
|
+
_Avoid_: generated tool, operation tool
|
|
12
|
+
|
|
13
|
+
**Custom tool**: A user-defined MCP tool in the tools directory, decorated with `@tool` from `fastmcp.tools`. Calls the supergraph via `execute_operation` or `get_client`.
|
|
14
|
+
_Avoid_: hand-written tool, user tool, manual tool
|
|
15
|
+
|
|
16
|
+
**DatalayerExtension**: The central library object. Acts as a FastMCP `Lifespan` (manages the supergraph client) and mounts both auto-tools and the `FileSystemProvider` for custom tools onto a FastMCP server via `mount()`.
|
|
17
|
+
_Avoid_: plugin, middleware, adapter, provider
|
|
18
|
+
|
|
19
|
+
**Operation registry**: Module-level dict mapping operation names to raw query strings. Populated at `mount()` time. Enables `execute_operation` to resolve query strings without a reference to the extension instance.
|
|
20
|
+
_Avoid_: query cache, operations map
|
|
21
|
+
|
|
22
|
+
**Tools directory**: The directory scanned by FastMCP's `FileSystemProvider` for custom tools. Configured via `tools.dir` in `mcp-config.yaml`.
|
|
23
|
+
_Avoid_: custom tools folder, plugins directory
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: redhat-datalayer-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastMCP runtime — turn GraphQL operations into MCP tools
|
|
5
|
+
Author: Red Hat DataLayer Team
|
|
6
|
+
Maintainer-email: Mayur Deshmukh <mdeshmuk@redhat.com>, Pranav Advani <padvani@redhat.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: fastmcp>=3.0
|
|
10
|
+
Requires-Dist: graphql-core>=3.2
|
|
11
|
+
Requires-Dist: pydantic>=2
|
|
12
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
13
|
+
Requires-Dist: pyyaml>=6
|
|
14
|
+
Requires-Dist: redhat-datalayer-graphql>=0.1.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# datalayer-mcp
|
|
18
|
+
|
|
19
|
+
MCP runtime library for the Datalayer platform. Turns curated GraphQL operation files into [FastMCP](https://gofastmcp.com/) tools backed by the Red Hat GraphQL **supergraph** via [`datalayer-graphql`](../datalayer-graphql/).
|
|
20
|
+
|
|
21
|
+
Each `.graphql` file in your operations directory becomes one MCP tool. Custom tools can reuse the same supergraph client for computed or aggregated logic.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Config-driven bootstrap** — `mcp-config.yaml` for supergraph URL, auth, and paths
|
|
26
|
+
- **Operation discovery** — scans a directory for `*.graphql` files at startup
|
|
27
|
+
- **Auto tool registration** — one MCP tool per named GraphQL operation
|
|
28
|
+
- **Variable schemas** — GraphQL variables mapped to Pydantic models → JSON Schema for MCP clients
|
|
29
|
+
- **Custom tool support** — `@tool` handlers (from `fastmcp.tools`) with shared `get_client()`
|
|
30
|
+
- **`.env` loading** — bearer token from a file next to your config (no manual `export`)
|
|
31
|
+
- **Structured errors** — federation-aware `GraphQLError` wrapped as `OperationError`
|
|
32
|
+
- **Session-managed client** — reuses HTTP connections via `async with SupergraphClient`
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Python 3.13+
|
|
37
|
+
- [uv](https://docs.astral.sh/uv/) (recommended)
|
|
38
|
+
- [`datalayer-graphql`](../datalayer-graphql/) (workspace dependency)
|
|
39
|
+
- VPN access for internal endpoints (e.g. `vpn.graphql.stage.redhat.com`)
|
|
40
|
+
- Valid GraphQL bearer token with `api.graphql` scope
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
From the monorepo root:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv sync --package datalayer-mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With dev dependencies (for running tests):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv sync --package datalayer-mcp --group dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
As a workspace dependency in another package:
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
dependencies = ["datalayer-mcp"]
|
|
60
|
+
|
|
61
|
+
[tool.uv.sources]
|
|
62
|
+
datalayer-mcp = { workspace = true }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
### 1. Project layout
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
my-mcp-server/
|
|
71
|
+
├── mcp-config.yaml
|
|
72
|
+
├── .env # SUPERGRAPH_TOKEN (gitignored)
|
|
73
|
+
├── server.py
|
|
74
|
+
├── operations/
|
|
75
|
+
│ └── Cves.graphql
|
|
76
|
+
└── tools/
|
|
77
|
+
├── __init__.py
|
|
78
|
+
└── custom.py # optional
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A complete working example lives in [`examples/mcp/`](../../examples/mcp/) at the monorepo root.
|
|
82
|
+
|
|
83
|
+
### 2. Configure supergraph connection
|
|
84
|
+
|
|
85
|
+
**`mcp-config.yaml`**
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
supergraph:
|
|
89
|
+
url: https://vpn.graphql.stage.redhat.com
|
|
90
|
+
token_env: SUPERGRAPH_TOKEN
|
|
91
|
+
client_name: rh-datalayer-platform-poc
|
|
92
|
+
verify_ssl: false
|
|
93
|
+
extra_headers: {}
|
|
94
|
+
|
|
95
|
+
operations:
|
|
96
|
+
directory: operations
|
|
97
|
+
|
|
98
|
+
tools:
|
|
99
|
+
dir: tools # optional directory of custom @tool files
|
|
100
|
+
include_response_headers: false
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**`.env`** (same directory as `mcp-config.yaml`):
|
|
104
|
+
|
|
105
|
+
```env
|
|
106
|
+
SUPERGRAPH_TOKEN=eyJ...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Copy from the example:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cp examples/mcp/.env.example examples/mcp/.env
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. Add a GraphQL operation
|
|
116
|
+
|
|
117
|
+
**`operations/Cves.graphql`**
|
|
118
|
+
|
|
119
|
+
```graphql
|
|
120
|
+
# List CVEs with pagination
|
|
121
|
+
query Cves($first: Int!) {
|
|
122
|
+
cves(first: $first) {
|
|
123
|
+
totalCount
|
|
124
|
+
edges {
|
|
125
|
+
node {
|
|
126
|
+
title
|
|
127
|
+
url
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Rules for operation files:
|
|
135
|
+
|
|
136
|
+
- Exactly **one named operation** per file (query, mutation, or subscription)
|
|
137
|
+
- The operation name in the GraphQL document becomes the MCP tool name (`Cves`)
|
|
138
|
+
- GraphQL docstrings (`"""..."""` or `"..."`) or leading `#` comment lines become the tool description
|
|
139
|
+
- The filename does not have to match the operation name
|
|
140
|
+
|
|
141
|
+
### 4. Bootstrap the server
|
|
142
|
+
|
|
143
|
+
**`server.py`**
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from pathlib import Path
|
|
147
|
+
|
|
148
|
+
from datalayer_mcp import create_server
|
|
149
|
+
|
|
150
|
+
CONFIG = Path(__file__).resolve().parent / "mcp-config.yaml"
|
|
151
|
+
mcp = create_server(config_path=CONFIG)
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
mcp.run()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use `Path(__file__)` so the config path works regardless of shell working directory.
|
|
158
|
+
|
|
159
|
+
### 5. Run
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv run --package datalayer-mcp examples/mcp/server.py
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The server starts on **stdio** (standard MCP transport). After the FastMCP banner it waits for an MCP client — that idle state is normal.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Architecture
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
mcp-config.yaml + .env
|
|
173
|
+
│
|
|
174
|
+
▼
|
|
175
|
+
create_server() ───► DatalayerExtension(config_path)
|
|
176
|
+
│
|
|
177
|
+
┌──────────────┴──────────────┐
|
|
178
|
+
▼ ▼
|
|
179
|
+
FastMCP(lifespan=ext) ext.mount(mcp)
|
|
180
|
+
│ │
|
|
181
|
+
▼ (on server run) ├── auto-tools (LocalProvider)
|
|
182
|
+
SupergraphClient ├── operation registry
|
|
183
|
+
in lifespan_context └── FileSystemProvider (tools.dir)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Auto-generated vs custom tools
|
|
187
|
+
|
|
188
|
+
| Type | Source | Example |
|
|
189
|
+
|------|--------|---------|
|
|
190
|
+
| **Auto** | Each `operations/*.graphql` file | `Cves` — runs the raw GraphQL operation |
|
|
191
|
+
| **Custom** | Python files in `tools.dir` | `cve_count` — calls `Cves` internally, returns a summary |
|
|
192
|
+
|
|
193
|
+
**Auto tool** — registered from `Cves.graphql`:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# Internally: handler(variables: CvesVariables) → client.execute("Cves", ...)
|
|
197
|
+
# Returns: { "data": { "cves": { ... } } }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Custom tool** — `tools/custom.py`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from datalayer_mcp import get_client
|
|
204
|
+
from fastmcp.tools import tool
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@tool
|
|
208
|
+
async def cve_count(first: int = 10) -> dict:
|
|
209
|
+
"""Return total CVE count from the Cves operation."""
|
|
210
|
+
client = get_client()
|
|
211
|
+
result = await client.execute(
|
|
212
|
+
operation_name="Cves",
|
|
213
|
+
variables={"first": first},
|
|
214
|
+
)
|
|
215
|
+
return {"total_count": result.data["cves"]["totalCount"]}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Custom tools live in the directory named by `tools.dir`. FastMCP's `FileSystemProvider` discovers `@tool` functions automatically; the files do not import the server.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Configuration reference
|
|
223
|
+
|
|
224
|
+
All paths in `mcp-config.yaml` are resolved **relative to the config file's directory**, not the shell cwd.
|
|
225
|
+
|
|
226
|
+
### `supergraph`
|
|
227
|
+
|
|
228
|
+
| Field | Type | Default | Description |
|
|
229
|
+
|-------|------|---------|-------------|
|
|
230
|
+
| `url` | URL | — | Supergraph endpoint |
|
|
231
|
+
| `token_env` | string | `SUPERGRAPH_TOKEN` | Env var name for bearer token |
|
|
232
|
+
| `client_name` | string | `rh-datalayer-platform-poc` | Apollo `apollographql-client-name` header |
|
|
233
|
+
| `client_version` | string | `latest` | Apollo client version header |
|
|
234
|
+
| `timeout` | float | `30.0` | HTTP timeout in seconds |
|
|
235
|
+
| `verify_ssl` | bool | `true` | TLS verification (`false` for VPN/internal CAs) |
|
|
236
|
+
| `extra_headers` | object | `{}` | Additional HTTP headers on every request |
|
|
237
|
+
|
|
238
|
+
### `operations`
|
|
239
|
+
|
|
240
|
+
| Field | Type | Default | Description |
|
|
241
|
+
|-------|------|---------|-------------|
|
|
242
|
+
| `directory` | string | `operations` | Folder containing `*.graphql` files |
|
|
243
|
+
|
|
244
|
+
### `tools`
|
|
245
|
+
|
|
246
|
+
| Field | Type | Default | Description |
|
|
247
|
+
|-------|------|---------|-------------|
|
|
248
|
+
| `dir` | string \| null | `null` | Directory of custom `@tool` files, relative to the config file |
|
|
249
|
+
| `include_response_headers` | bool | `false` | Include router HTTP headers in auto-tool responses |
|
|
250
|
+
|
|
251
|
+
When `include_response_headers: true`, auto-tools return:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"data": { },
|
|
256
|
+
"response_headers": { "content-type": "application/json" },
|
|
257
|
+
"extensions": { }
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Environment variables
|
|
262
|
+
|
|
263
|
+
Loaded from `{config_dir}/.env`, then `{cwd}/.env` as fallback. Existing shell variables are **not** overwritten.
|
|
264
|
+
|
|
265
|
+
| Variable | Required | Description |
|
|
266
|
+
|----------|----------|-------------|
|
|
267
|
+
| `SUPERGRAPH_TOKEN` | Yes (for live calls) | Bearer JWT with `api.graphql` scope |
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## API reference
|
|
272
|
+
|
|
273
|
+
### Public exports
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from datalayer_mcp import (
|
|
277
|
+
create_server,
|
|
278
|
+
DatalayerExtension,
|
|
279
|
+
execute_operation,
|
|
280
|
+
get_client,
|
|
281
|
+
SupergraphError,
|
|
282
|
+
SupergraphConnectionError,
|
|
283
|
+
SupergraphHTTPError,
|
|
284
|
+
GraphQLError,
|
|
285
|
+
GraphQLErrorDetail,
|
|
286
|
+
OperationError,
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `create_server(name="datalayer_mcp", *, config_path, **fastmcp_kwargs)`
|
|
291
|
+
|
|
292
|
+
Loads config, constructs a `DatalayerExtension`, passes it as `lifespan` to a new `FastMCP` instance (forwarding `name` and extra `fastmcp_kwargs`), mounts auto-tools and custom tools from `tools.dir`, and returns the `FastMCP` instance.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from pathlib import Path
|
|
296
|
+
from datalayer_mcp import create_server
|
|
297
|
+
|
|
298
|
+
mcp = create_server("my-mcp", config_path=Path("mcp-config.yaml").resolve())
|
|
299
|
+
mcp.run()
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Startup validation:**
|
|
303
|
+
|
|
304
|
+
- Operations directory must exist
|
|
305
|
+
- Each `.graphql` file must have valid syntax and exactly one named operation
|
|
306
|
+
- No duplicate operation names across files
|
|
307
|
+
- MCP parser and `datalayer-graphql` loader must agree on operation names
|
|
308
|
+
- Custom `@tool` names in `tools.dir` must not collide with operation names
|
|
309
|
+
|
|
310
|
+
### `get_client()`
|
|
311
|
+
|
|
312
|
+
Returns the lifespan-managed `SupergraphClient`. Only available from within a tool handler while the MCP server is running — reads from FastMCP's `lifespan_context` via `get_context()`, with no module-level client global.
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from datalayer_mcp import get_client
|
|
316
|
+
|
|
317
|
+
client = get_client()
|
|
318
|
+
result = await client.execute(operation_name="Cves", variables={"first": 10})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Raises `RuntimeError` if called outside an active MCP server session.
|
|
322
|
+
|
|
323
|
+
### `execute_operation(operation_name, variables=None)`
|
|
324
|
+
|
|
325
|
+
Executes a registered GraphQL operation against the supergraph using the query string stored in the module-level operation registry. Must be called within an active tool handler (where `get_client()` is available).
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from datalayer_mcp import execute_operation
|
|
329
|
+
|
|
330
|
+
result = await execute_operation("Cves", {"first": 10})
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Raises `ValueError` if the operation name is not registered, or `RuntimeError` if called outside an active MCP server session.
|
|
334
|
+
|
|
335
|
+
### `DatalayerExtension(config_path)`
|
|
336
|
+
|
|
337
|
+
A FastMCP `Lifespan`: opens a session-managed `SupergraphClient` on server startup and closes it on shutdown, storing it under `lifespan_context["supergraph_client"]`. `create_server()` wires this up for you; construct it directly for custom `FastMCP` setups:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from fastmcp import FastMCP
|
|
341
|
+
from datalayer_mcp import DatalayerExtension
|
|
342
|
+
|
|
343
|
+
ext = DatalayerExtension("mcp-config.yaml")
|
|
344
|
+
mcp = FastMCP("my-server", lifespan=ext)
|
|
345
|
+
ext.mount(mcp)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## GraphQL variables → MCP input schema
|
|
351
|
+
|
|
352
|
+
Operation variables are parsed with `graphql-core` and converted to dynamic Pydantic models:
|
|
353
|
+
|
|
354
|
+
| GraphQL type | Python / JSON Schema |
|
|
355
|
+
|--------------|---------------------|
|
|
356
|
+
| `String`, `ID` | `string` |
|
|
357
|
+
| `Int` | `integer` |
|
|
358
|
+
| `Float` | `number` |
|
|
359
|
+
| `Boolean` | `boolean` |
|
|
360
|
+
| `[T]` | `array` |
|
|
361
|
+
| Input object | `object` (untyped in MVP) |
|
|
362
|
+
| Enum | `string` (no enum constraint in MVP) |
|
|
363
|
+
|
|
364
|
+
Non-null (`!`) variables are required. Auto-tools accept a single `variables` object matching the Pydantic model (e.g. `CvesVariables(first: int)`).
|
|
365
|
+
|
|
366
|
+
Apollo Router validates input objects and enums at execution time even when MCP schemas are loose.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Error handling
|
|
371
|
+
|
|
372
|
+
GraphQL and HTTP errors from `datalayer-graphql` are wrapped in `OperationError` for auto-generated tools:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
from datalayer_mcp import OperationError
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
...
|
|
379
|
+
except OperationError as exc:
|
|
380
|
+
print(exc.payload)
|
|
381
|
+
# {
|
|
382
|
+
# "error_type": "GraphQLError",
|
|
383
|
+
# "message": "...",
|
|
384
|
+
# "errors": [{"message": "...", "code": "SUBREQUEST_HTTP_ERROR", "service": "..."}],
|
|
385
|
+
# "partial_data": ...
|
|
386
|
+
# }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
| Exception | When |
|
|
390
|
+
|-----------|------|
|
|
391
|
+
| `SupergraphConnectionError` | VPN down, network failure |
|
|
392
|
+
| `SupergraphHTTPError` | HTTP 401/403/503 (`status_code` attribute) |
|
|
393
|
+
| `GraphQLError` | GraphQL errors in response (may include partial federation data) |
|
|
394
|
+
| `OperationError` | Wrapper raised by auto-tools |
|
|
395
|
+
|
|
396
|
+
In custom tools, catch `GraphQLError` directly or let errors propagate to the MCP client.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Connect to Cursor
|
|
401
|
+
|
|
402
|
+
Add to Cursor MCP settings (`.cursor/mcp.json` or Settings → MCP):
|
|
403
|
+
|
|
404
|
+
```json
|
|
405
|
+
{
|
|
406
|
+
"mcpServers": {
|
|
407
|
+
"datalayer": {
|
|
408
|
+
"command": "uv",
|
|
409
|
+
"args": [
|
|
410
|
+
"run",
|
|
411
|
+
"--package",
|
|
412
|
+
"datalayer-mcp",
|
|
413
|
+
"examples/mcp/server.py"
|
|
414
|
+
],
|
|
415
|
+
"cwd": "/absolute/path/to/datalayer-python-monorepo"
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Put `SUPERGRAPH_TOKEN` in `examples/mcp/.env`. Restart Cursor after changing MCP config.
|
|
422
|
+
|
|
423
|
+
Available tools with the example project:
|
|
424
|
+
|
|
425
|
+
- **`Cves`** — auto-generated from `poc/operations/Cves.graphql`
|
|
426
|
+
- **`cve_count`** — custom tool from `poc/tools/custom.py`
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Development
|
|
431
|
+
|
|
432
|
+
### Package layout
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
packages/datalayer-mcp/
|
|
436
|
+
├── src/datalayer_mcp/
|
|
437
|
+
│ ├── __init__.py # Public API
|
|
438
|
+
│ ├── config.py # mcp-config.yaml → McpConfig
|
|
439
|
+
│ ├── env.py # .env loading
|
|
440
|
+
│ ├── parse.py # .graphql → ParsedOperation
|
|
441
|
+
│ ├── scan.py # AST collision detection for tools.dir
|
|
442
|
+
│ ├── schema.py # GraphQL variables → Pydantic models
|
|
443
|
+
│ ├── tools.py # Auto-tool registration
|
|
444
|
+
│ ├── server.py # create_server() orchestration
|
|
445
|
+
│ ├── extension.py # DatalayerExtension (FastMCP Lifespan)
|
|
446
|
+
│ ├── runtime.py # get_client() and execute_operation()
|
|
447
|
+
│ └── errors.py # OperationError formatting
|
|
448
|
+
├── tests/ # Unit tests (no network)
|
|
449
|
+
└── pyproject.toml
|
|
450
|
+
|
|
451
|
+
Runnable POC server: `examples/mcp/` at the monorepo root.
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Run tests
|
|
455
|
+
|
|
456
|
+
From monorepo root:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
uv run --package datalayer-mcp pytest packages/datalayer-mcp/tests -v
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
From the package directory:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
cd packages/datalayer-mcp
|
|
466
|
+
uv run pytest tests -v
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Tests cover env loading, operation parsing, schema mapping, error formatting, duplicate detection, and mocked tool handlers. No VPN or live supergraph required.
|
|
470
|
+
|
|
471
|
+
### Smoke-test server bootstrap
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
cp examples/mcp/.env.example examples/mcp/.env # set SUPERGRAPH_TOKEN
|
|
475
|
+
|
|
476
|
+
uv run --package datalayer-mcp python -c "
|
|
477
|
+
from pathlib import Path
|
|
478
|
+
from datalayer_mcp import create_server
|
|
479
|
+
create_server(config_path=Path('examples/mcp/mcp-config.yaml').resolve())
|
|
480
|
+
print('create_server OK')
|
|
481
|
+
"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Run the example server
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
uv run --package datalayer-mcp examples/mcp/server.py
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Live supergraph tests
|
|
491
|
+
|
|
492
|
+
Live integration tests live in `datalayer-graphql`:
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
cd packages/datalayer-graphql
|
|
496
|
+
cp .env.example .env
|
|
497
|
+
uv run pytest tests/test_smoke_supergraph.py -v -s
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Adding a new auto-tool
|
|
501
|
+
|
|
502
|
+
1. Create `operations/MyOperation.graphql` with one named operation
|
|
503
|
+
2. Restart the MCP server
|
|
504
|
+
3. The tool appears automatically as `MyOperation`
|
|
505
|
+
|
|
506
|
+
### Adding a custom tool
|
|
507
|
+
|
|
508
|
+
1. Create a `.py` file in the directory named by `tools.dir` in `mcp-config.yaml`
|
|
509
|
+
2. Use `@tool` (from `fastmcp.tools`) and `get_client()` as shown above
|
|
510
|
+
3. Restart the MCP server; the tool is discovered automatically
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Obtaining a supergraph token
|
|
515
|
+
|
|
516
|
+
Tokens expire after ~15 minutes. Fetch a client-credentials token from Red Hat SSO:
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
curl -X POST \
|
|
520
|
+
"https://sso.stage.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \
|
|
521
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
522
|
+
-d "grant_type=client_credentials" \
|
|
523
|
+
-d "client_id=YOUR_CLIENT_ID" \
|
|
524
|
+
-d "client_secret=YOUR_SECRET" \
|
|
525
|
+
-d "scope=api.graphql"
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Copy `access_token` into `.env` as `SUPERGRAPH_TOKEN`.
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Relationship to datalayer-graphql
|
|
533
|
+
|
|
534
|
+
| Concern | Package |
|
|
535
|
+
|---------|---------|
|
|
536
|
+
| HTTP client, auth, Apollo headers | `datalayer-graphql` |
|
|
537
|
+
| Operation file loading (registry) | `datalayer-graphql.load_operations()` |
|
|
538
|
+
| MCP tool registration, config, `.env` | `datalayer-mcp` |
|
|
539
|
+
| Variable → JSON Schema for MCP | `datalayer-mcp` |
|
|
540
|
+
| Custom tool integration | `datalayer-mcp` |
|
|
541
|
+
|
|
542
|
+
`datalayer-mcp` does not implement GraphQL HTTP directly — all supergraph communication goes through `SupergraphClient`.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Troubleshooting
|
|
547
|
+
|
|
548
|
+
| Problem | Likely cause | Fix |
|
|
549
|
+
|---------|--------------|-----|
|
|
550
|
+
| `FileNotFoundError: mcp-config.yaml` | Wrong cwd | Use `Path(__file__).parent / "mcp-config.yaml"` |
|
|
551
|
+
| `Missing env var: SUPERGRAPH_TOKEN` | No `.env` or expired token | Copy `.env.example`, refresh token |
|
|
552
|
+
| `FileNotFoundError: tools.dir not found` | `tools.dir` path missing | Create the directory or drop the `tools.dir` key |
|
|
553
|
+
| `ValueError: Custom tool name collides` | `@tool` name matches an operation | Rename the custom tool or the operation |
|
|
554
|
+
| `ValidationError: instructions` | `FastMCP(name, lifespan)` as positional arg | Use `FastMCP(name, lifespan=lifespan)` |
|
|
555
|
+
| `Operation registry mismatch` | Parser disagreement | Check file encoding; both loaders strip whitespace |
|
|
556
|
+
| Server idle after banner | Normal stdio behavior | Connect via Cursor MCP client |
|
|
557
|
+
| Tool call fails | VPN off or bad token | Connect to VPN, refresh `SUPERGRAPH_TOKEN` |
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## License
|
|
562
|
+
|
|
563
|
+
Internal Red Hat / Datalayer project.
|