vectorstep-gateway-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.
- vectorstep_gateway_mcp-0.1.0/LICENSE +21 -0
- vectorstep_gateway_mcp-0.1.0/PKG-INFO +85 -0
- vectorstep_gateway_mcp-0.1.0/README.md +54 -0
- vectorstep_gateway_mcp-0.1.0/pyproject.toml +55 -0
- vectorstep_gateway_mcp-0.1.0/setup.cfg +4 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/__init__.py +0 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/__main__.py +13 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/client.py +75 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/errors.py +76 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/server.py +9 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/tools/__init__.py +1 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/tools/read.py +86 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/tools/write.py +98 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/PKG-INFO +85 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/SOURCES.txt +22 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/dependency_links.txt +1 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/entry_points.txt +2 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/requires.txt +7 -0
- vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/top_level.txt +1 -0
- vectorstep_gateway_mcp-0.1.0/tests/test_client.py +142 -0
- vectorstep_gateway_mcp-0.1.0/tests/test_e2e.py +179 -0
- vectorstep_gateway_mcp-0.1.0/tests/test_stub.py +5 -0
- vectorstep_gateway_mcp-0.1.0/tests/test_tools_read.py +189 -0
- vectorstep_gateway_mcp-0.1.0/tests/test_tools_write.py +176 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Dalton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vectorstep-gateway-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server exposing VectorStep Gateway's agent-management and introspection surface to MCP clients (Claude Code/Desktop).
|
|
5
|
+
Author-email: Alex Dalton <alex@vectorstep.io>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://vectorstep.io
|
|
8
|
+
Project-URL: Documentation, https://vectorstep.io/docs/integrations/mcp/
|
|
9
|
+
Project-URL: Changelog, https://vectorstep.io/docs/about/status-and-support/
|
|
10
|
+
Keywords: mcp,vectorstep,ai,agents,gateway,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: mcp==1.28.1
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# VectorStep Gateway MCP
|
|
33
|
+
|
|
34
|
+
An [MCP](https://modelcontextprotocol.io) server that exposes [VectorStep Gateway](https://github.com/bantex01/VectorStep-Gateway) — agent authoring and inspection — to MCP clients such as Claude Code and Claude Desktop.
|
|
35
|
+
|
|
36
|
+
## What it is
|
|
37
|
+
|
|
38
|
+
A separate, standalone repository and process with no import-level dependency on the Gateway — the two are coupled only over HTTP, so each can be developed, versioned, and deployed independently.
|
|
39
|
+
|
|
40
|
+
It lets an MCP client create and edit agents (`agent.yaml` + `soul.md`, with the same validation the gateway itself uses), inspect what's available (configured MCP tool servers and their tools, configured LLM providers), and read gateway health/metrics. Pipelines/steps are not authored here — that's the job of the companion `VectorStep-Service-MCP`; the two have clean, non-overlapping tool sets.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python3 -m venv .venv
|
|
46
|
+
.venv/bin/pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
CI runs this repo's test suite on every push and pull request
|
|
50
|
+
(`.github/workflows/tests.yml`).
|
|
51
|
+
|
|
52
|
+
Then register it with your MCP client, pointing at a running Gateway instance:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
claude mcp add vectorstep-gateway \
|
|
56
|
+
--env GATEWAY_BASE_URL=http://127.0.0.1:18780 \
|
|
57
|
+
--env GATEWAY_OPERATOR_TOKEN=<your-operator-token> \
|
|
58
|
+
-- /absolute/path/to/VectorStep-Gateway-MCP/.venv/bin/python -m vectorstep_gateway_mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Full install, client config (Claude Code, Claude Desktop, MCP Inspector), and tool inventory: [MCP servers](https://vectorstep.io/docs/integrations/mcp/).
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
Full docs at [vectorstep.io](https://vectorstep.io/docs/):
|
|
66
|
+
|
|
67
|
+
| Section | Covers |
|
|
68
|
+
|---|---|
|
|
69
|
+
| [MCP servers](https://vectorstep.io/docs/integrations/mcp/) | Install, client config, tool inventory, write-path design notes for both MCP servers |
|
|
70
|
+
| [Gateway](https://vectorstep.io/docs/gateway/agents/) | Agent authoring (`agent.yaml`, `soul.md`) that this server exposes |
|
|
71
|
+
|
|
72
|
+
## The ecosystem
|
|
73
|
+
|
|
74
|
+
| Repo | Role |
|
|
75
|
+
|---|---|
|
|
76
|
+
| **VectorStep** | The orchestration service: webhook intake, pipeline runner, trust gating, UI, analytics |
|
|
77
|
+
| **VectorStep-Gateway** | WebSocket gateway that runs agents: LLM providers, MCP tools, the full agentic loop |
|
|
78
|
+
| **VectorStep-Service-MCP** | MCP server exposing pipeline authoring, run inspection and analytics to Claude Code/Desktop |
|
|
79
|
+
| **VectorStep-Gateway-MCP** | MCP server for authoring and inspecting Gateway agents |
|
|
80
|
+
|
|
81
|
+
## Licence
|
|
82
|
+
|
|
83
|
+
VectorStep Gateway MCP is proprietary software, free to download and use under the terms in [`LICENSE`](LICENSE). The source is not publicly distributed and this repository is private.
|
|
84
|
+
|
|
85
|
+
Bug reports, questions, and feature requests are welcome at **alex@vectorstep.io**. Code contributions are not accepted — see clause 6 of the licence. For a suspected vulnerability, follow [`SECURITY.md`](SECURITY.md) rather than emailing the address above.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# VectorStep Gateway MCP
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that exposes [VectorStep Gateway](https://github.com/bantex01/VectorStep-Gateway) — agent authoring and inspection — to MCP clients such as Claude Code and Claude Desktop.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
A separate, standalone repository and process with no import-level dependency on the Gateway — the two are coupled only over HTTP, so each can be developed, versioned, and deployed independently.
|
|
8
|
+
|
|
9
|
+
It lets an MCP client create and edit agents (`agent.yaml` + `soul.md`, with the same validation the gateway itself uses), inspect what's available (configured MCP tool servers and their tools, configured LLM providers), and read gateway health/metrics. Pipelines/steps are not authored here — that's the job of the companion `VectorStep-Service-MCP`; the two have clean, non-overlapping tool sets.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python3 -m venv .venv
|
|
15
|
+
.venv/bin/pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
CI runs this repo's test suite on every push and pull request
|
|
19
|
+
(`.github/workflows/tests.yml`).
|
|
20
|
+
|
|
21
|
+
Then register it with your MCP client, pointing at a running Gateway instance:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
claude mcp add vectorstep-gateway \
|
|
25
|
+
--env GATEWAY_BASE_URL=http://127.0.0.1:18780 \
|
|
26
|
+
--env GATEWAY_OPERATOR_TOKEN=<your-operator-token> \
|
|
27
|
+
-- /absolute/path/to/VectorStep-Gateway-MCP/.venv/bin/python -m vectorstep_gateway_mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Full install, client config (Claude Code, Claude Desktop, MCP Inspector), and tool inventory: [MCP servers](https://vectorstep.io/docs/integrations/mcp/).
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
Full docs at [vectorstep.io](https://vectorstep.io/docs/):
|
|
35
|
+
|
|
36
|
+
| Section | Covers |
|
|
37
|
+
|---|---|
|
|
38
|
+
| [MCP servers](https://vectorstep.io/docs/integrations/mcp/) | Install, client config, tool inventory, write-path design notes for both MCP servers |
|
|
39
|
+
| [Gateway](https://vectorstep.io/docs/gateway/agents/) | Agent authoring (`agent.yaml`, `soul.md`) that this server exposes |
|
|
40
|
+
|
|
41
|
+
## The ecosystem
|
|
42
|
+
|
|
43
|
+
| Repo | Role |
|
|
44
|
+
|---|---|
|
|
45
|
+
| **VectorStep** | The orchestration service: webhook intake, pipeline runner, trust gating, UI, analytics |
|
|
46
|
+
| **VectorStep-Gateway** | WebSocket gateway that runs agents: LLM providers, MCP tools, the full agentic loop |
|
|
47
|
+
| **VectorStep-Service-MCP** | MCP server exposing pipeline authoring, run inspection and analytics to Claude Code/Desktop |
|
|
48
|
+
| **VectorStep-Gateway-MCP** | MCP server for authoring and inspecting Gateway agents |
|
|
49
|
+
|
|
50
|
+
## Licence
|
|
51
|
+
|
|
52
|
+
VectorStep Gateway MCP is proprietary software, free to download and use under the terms in [`LICENSE`](LICENSE). The source is not publicly distributed and this repository is private.
|
|
53
|
+
|
|
54
|
+
Bug reports, questions, and feature requests are welcome at **alex@vectorstep.io**. Code contributions are not accepted — see clause 6 of the licence. For a suspected vulnerability, follow [`SECURITY.md`](SECURITY.md) rather than emailing the address above.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vectorstep-gateway-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server exposing VectorStep Gateway's agent-management and introspection surface to MCP clients (Claude Code/Desktop)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Alex Dalton", email = "alex@vectorstep.io" }]
|
|
10
|
+
keywords = ["mcp", "vectorstep", "ai", "agents", "gateway", "llm"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: System Administrators",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
"Topic :: System :: Monitoring",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp==1.28.1",
|
|
25
|
+
"httpx>=0.27",
|
|
26
|
+
"pyyaml>=6.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-asyncio>=0.24",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://vectorstep.io"
|
|
37
|
+
Documentation = "https://vectorstep.io/docs/integrations/mcp/"
|
|
38
|
+
Changelog = "https://vectorstep.io/docs/about/status-and-support/"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
vectorstep-gateway-mcp = "vectorstep_gateway_mcp.__main__:main"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["setuptools>=77"]
|
|
45
|
+
build-backend = "setuptools.build_meta"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
vectorstep_gateway_mcp = ["docs/*.md"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Entry point for the VectorStep Gateway MCP server. Runs over stdio (Claude Code /
|
|
2
|
+
Desktop) — see the README for GATEWAY_BASE_URL / GATEWAY_OPERATOR_TOKEN configuration."""
|
|
3
|
+
|
|
4
|
+
from . import tools # noqa: F401 — importing registers every @mcp.tool()
|
|
5
|
+
from .server import mcp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
mcp.run(transport="stdio")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Thin HTTP client for talking to a VectorStep Gateway instance.
|
|
2
|
+
|
|
3
|
+
This is the ONLY coupling to VectorStep Gateway (SPEC-gateway-mcp.md §2.3): no
|
|
4
|
+
gateway code is imported anywhere in this package. Configured from
|
|
5
|
+
GATEWAY_BASE_URL / GATEWAY_OPERATOR_TOKEN — see the README.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .errors import GatewayAPIError, error_from_response
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GatewayClient:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
base_url: str | None = None,
|
|
19
|
+
token: str | None = None,
|
|
20
|
+
timeout: float = 30.0,
|
|
21
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
22
|
+
):
|
|
23
|
+
self.base_url = (base_url or os.environ.get("GATEWAY_BASE_URL") or "http://127.0.0.1:18780").rstrip("/")
|
|
24
|
+
self.token = token if token is not None else os.environ.get("GATEWAY_OPERATOR_TOKEN")
|
|
25
|
+
self.timeout = timeout
|
|
26
|
+
# Injectable for tests (httpx.MockTransport) — None uses the real network.
|
|
27
|
+
self.transport = transport
|
|
28
|
+
|
|
29
|
+
def _headers(self) -> dict:
|
|
30
|
+
return {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
|
31
|
+
|
|
32
|
+
async def request(self, method: str, path: str, **kwargs) -> dict:
|
|
33
|
+
url = f"{self.base_url}{path}"
|
|
34
|
+
try:
|
|
35
|
+
async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as http_client:
|
|
36
|
+
resp = await http_client.request(method, url, headers=self._headers(), **kwargs)
|
|
37
|
+
except httpx.RequestError as exc:
|
|
38
|
+
raise GatewayAPIError(
|
|
39
|
+
"network", f"Could not reach VectorStep Gateway at {self.base_url}: {exc}",
|
|
40
|
+
) from exc
|
|
41
|
+
|
|
42
|
+
if resp.status_code >= 400:
|
|
43
|
+
raise error_from_response(resp)
|
|
44
|
+
|
|
45
|
+
if not resp.content:
|
|
46
|
+
return {}
|
|
47
|
+
return resp.json()
|
|
48
|
+
|
|
49
|
+
async def get(self, path: str, params: dict | None = None) -> dict:
|
|
50
|
+
return await self.request("GET", path, params=params)
|
|
51
|
+
|
|
52
|
+
async def post(self, path: str, json: dict | None = None) -> dict:
|
|
53
|
+
return await self.request("POST", path, json=json)
|
|
54
|
+
|
|
55
|
+
async def put(self, path: str, json: dict | None = None) -> dict:
|
|
56
|
+
return await self.request("PUT", path, json=json)
|
|
57
|
+
|
|
58
|
+
async def delete(self, path: str) -> dict:
|
|
59
|
+
return await self.request("DELETE", path)
|
|
60
|
+
|
|
61
|
+
async def get_text(self, path: str) -> str:
|
|
62
|
+
"""Like get(), but for endpoints that don't return JSON — currently
|
|
63
|
+
just /metrics, which is Prometheus exposition text."""
|
|
64
|
+
url = f"{self.base_url}{path}"
|
|
65
|
+
try:
|
|
66
|
+
async with httpx.AsyncClient(timeout=self.timeout, transport=self.transport) as http_client:
|
|
67
|
+
resp = await http_client.get(url, headers=self._headers())
|
|
68
|
+
except httpx.RequestError as exc:
|
|
69
|
+
raise GatewayAPIError(
|
|
70
|
+
"network", f"Could not reach VectorStep Gateway at {self.base_url}: {exc}",
|
|
71
|
+
) from exc
|
|
72
|
+
|
|
73
|
+
if resp.status_code >= 400:
|
|
74
|
+
raise error_from_response(resp)
|
|
75
|
+
return resp.text
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Structured tool-error shape shared by every tool (SPEC-gateway-mcp.md §2.8):
|
|
2
|
+
|
|
3
|
+
{"error": {"type": "validation|not_found|collision|upstream|network",
|
|
4
|
+
"message": "...", "detail": {...}}}
|
|
5
|
+
|
|
6
|
+
Tools never let an httpx exception or traceback reach the caller — every
|
|
7
|
+
gateway HTTP call goes through GatewayClient, which raises GatewayAPIError;
|
|
8
|
+
tools catch that (via call_safe) and return its .to_dict() as the tool
|
|
9
|
+
result instead of raising.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
_STATUS_TO_TYPE = {
|
|
17
|
+
400: "validation",
|
|
18
|
+
404: "not_found",
|
|
19
|
+
409: "collision",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GatewayAPIError(Exception):
|
|
24
|
+
def __init__(self, error_type: str, message: str, detail: Any = None):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.error_type = error_type
|
|
27
|
+
self.message = message
|
|
28
|
+
self.detail = detail if detail is not None else {}
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
return {"error": {"type": self.error_type, "message": self.message, "detail": self.detail}}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def error_from_response(resp: httpx.Response) -> GatewayAPIError:
|
|
35
|
+
"""Map an HTTP error response from the gateway to a typed GatewayAPIError.
|
|
36
|
+
|
|
37
|
+
FastAPI's HTTPException always serialises to {"detail": <value>}, where
|
|
38
|
+
<value> is either a plain string (the gateway's older, pre-existing
|
|
39
|
+
endpoints) or a dict like {"type": "...", "message": "...", ...} for the
|
|
40
|
+
agent write endpoints (which know their own error type precisely — a
|
|
41
|
+
plain validation failure vs. a write that references an unconfigured
|
|
42
|
+
provider/MCP-server can both surface as a 400 with unrelated wording, so
|
|
43
|
+
status-code/message-sniffing alone can't tell them apart). Prefer that
|
|
44
|
+
explicit type; fall back to a status-code guess otherwise. Anything
|
|
45
|
+
unparseable still gets a typed, non-traceback error.
|
|
46
|
+
"""
|
|
47
|
+
status = resp.status_code
|
|
48
|
+
try:
|
|
49
|
+
body = resp.json()
|
|
50
|
+
except Exception:
|
|
51
|
+
body = {"raw": resp.text}
|
|
52
|
+
|
|
53
|
+
detail = body if isinstance(body, dict) else {"raw": body}
|
|
54
|
+
inner = detail.get("detail")
|
|
55
|
+
explicit_type = None
|
|
56
|
+
if isinstance(inner, str):
|
|
57
|
+
message = inner
|
|
58
|
+
elif isinstance(inner, dict):
|
|
59
|
+
message = inner.get("message") if isinstance(inner.get("message"), str) else None
|
|
60
|
+
explicit_type = inner.get("type") if isinstance(inner.get("type"), str) else None
|
|
61
|
+
else:
|
|
62
|
+
message = None
|
|
63
|
+
message = message or f"Gateway returned HTTP {status} for {resp.request.method} {resp.request.url.path}"
|
|
64
|
+
|
|
65
|
+
error_type = explicit_type or _STATUS_TO_TYPE.get(status, "upstream")
|
|
66
|
+
|
|
67
|
+
return GatewayAPIError(error_type, message, detail=detail)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def call_safe(coro) -> dict:
|
|
71
|
+
"""Await `coro`, converting any GatewayAPIError into its structured dict
|
|
72
|
+
form. Every tool's body should be `return await call_safe(client.get(...))`."""
|
|
73
|
+
try:
|
|
74
|
+
return await coro
|
|
75
|
+
except GatewayAPIError as exc:
|
|
76
|
+
return exc.to_dict()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Shared FastMCP server instance and Gateway client — imported by every tool
|
|
2
|
+
module so they register against the same server (see __main__.py)."""
|
|
3
|
+
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
|
|
6
|
+
from .client import GatewayClient
|
|
7
|
+
|
|
8
|
+
mcp = FastMCP("vectorstep-gateway")
|
|
9
|
+
client = GatewayClient()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import read, write # noqa: F401 — importing registers every @mcp.tool()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Read-only tools (SPEC-gateway-mcp.md §4 "Read tools (Phase A)"). Pure
|
|
2
|
+
GETs (plus the no-write validate_agent dry-run) against the gateway's REST
|
|
3
|
+
API — nothing here mutates agents/ or config.yaml.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ..errors import call_safe
|
|
7
|
+
from ..server import client, mcp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
async def list_agents() -> dict:
|
|
12
|
+
"""List every agent currently loaded by the gateway: name, model,
|
|
13
|
+
model_fallbacks, tools (the MCP servers/tool scopes it's wired to), and
|
|
14
|
+
`version` — a content hash over the agent's ENTIRE config, including
|
|
15
|
+
soul.md. `version` changes whenever agent.yaml or soul.md changes; VectorStep
|
|
16
|
+
uses it to scope calibration buckets, so two runs under different
|
|
17
|
+
`version`s are never pooled as evidence for the same track record. Does
|
|
18
|
+
not include soul.md content itself — use get_agent(name) for that."""
|
|
19
|
+
return await call_safe(client.get("/agents"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
async def get_agent(name: str) -> dict:
|
|
24
|
+
"""Get one agent's full definition: parsed config (model, model_fallbacks,
|
|
25
|
+
max_tokens, tools), the raw agent.yaml text (for round-trip editing), the
|
|
26
|
+
full soul.md content, and `version` — a content hash over the agent's
|
|
27
|
+
entire config including soul.md. `version` changes whenever agent.yaml or
|
|
28
|
+
soul.md changes; VectorStep uses it to scope calibration buckets, so two runs
|
|
29
|
+
under different `version`s are never pooled as evidence for the same
|
|
30
|
+
track record. 404 (not_found) if no agent by that name is loaded."""
|
|
31
|
+
return await call_safe(client.get(f"/agents/{name}"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
async def list_mcp_servers() -> dict:
|
|
36
|
+
"""List the gateway's configured MCP tool servers and their status
|
|
37
|
+
(running, pid, restart_count). An agent's `tools:` list references these
|
|
38
|
+
server names — a name here is what create_agent/update_agent's tools:
|
|
39
|
+
entries must match."""
|
|
40
|
+
return await call_safe(client.get("/mcp/servers"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
async def list_mcp_tools() -> dict:
|
|
45
|
+
"""List every tool available across all configured MCP servers, grouped
|
|
46
|
+
by server — name, registered (namespaced) name, description, and JSON
|
|
47
|
+
input schema for each. Useful for understanding what an agent scoped to
|
|
48
|
+
a given server can actually do."""
|
|
49
|
+
return await call_safe(client.get("/mcp/tools"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@mcp.tool()
|
|
53
|
+
async def list_providers() -> dict:
|
|
54
|
+
"""List the gateway's configured LLM providers: name, whether credentials
|
|
55
|
+
are configured (never the key itself), and the model-string prefix to
|
|
56
|
+
use when writing an agent's `model`/`model_fallbacks` (e.g.
|
|
57
|
+
'openrouter/deepseek/deepseek-chat'; a bare model name with no prefix
|
|
58
|
+
routes to Anthropic by default, so Anthropic's prefix is null). Does NOT
|
|
59
|
+
enumerate each provider's available model catalogue — only which
|
|
60
|
+
providers are configured and how to address them."""
|
|
61
|
+
return await call_safe(client.get("/providers"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _metrics_as_dict() -> dict:
|
|
65
|
+
text = await client.get_text("/metrics")
|
|
66
|
+
return {"metrics": text}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
async def get_metrics() -> dict:
|
|
71
|
+
"""Get the gateway's Prometheus metrics as raw exposition text (agent run
|
|
72
|
+
counts/durations, token usage, MCP tool call counts, session counts,
|
|
73
|
+
etc.) under the "metrics" key. This is the same text a Prometheus
|
|
74
|
+
scraper would pull from /metrics — not pre-aggregated JSON."""
|
|
75
|
+
return await call_safe(_metrics_as_dict())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp.tool()
|
|
79
|
+
async def validate_agent(agent_yaml: str, soul_md: str = "") -> dict:
|
|
80
|
+
"""Validate a candidate agent (agent.yaml + soul.md) against the gateway's
|
|
81
|
+
real schema AND its reference checks — model/model_fallbacks must map to
|
|
82
|
+
a configured provider, tools: must reference configured MCP servers —
|
|
83
|
+
WITHOUT writing anything. Returns {"valid": bool, "errors": [...]}. Use
|
|
84
|
+
this to iterate before calling create_agent/update_agent; soul_md can be
|
|
85
|
+
omitted for a quick agent.yaml-only check."""
|
|
86
|
+
return await call_safe(client.post("/agents/validate", json={"agent_yaml": agent_yaml, "soul_md": soul_md}))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Write / action tools (SPEC-gateway-mcp.md §4 "Write / action tools (Phase
|
|
2
|
+
C)"). These mutate the gateway's agents/ directory or trigger a reload —
|
|
3
|
+
unlike tools/read.py, which is pure reads (plus the no-write validate_agent).
|
|
4
|
+
|
|
5
|
+
Cross-cutting rules enforced here (§6, §9.4):
|
|
6
|
+
- create_agent never overwrites an existing name unless the caller
|
|
7
|
+
explicitly passes overwrite=True.
|
|
8
|
+
- update_agent 404s if the name doesn't exist yet; either agent_yaml or
|
|
9
|
+
soul_md may be omitted to leave that file untouched.
|
|
10
|
+
- delete_agent requires the caller to explicitly pass confirm=True —
|
|
11
|
+
without it, nothing is sent to the gateway at all — and returns the
|
|
12
|
+
deleted agent.yaml/soul.md so the operation is auditable/recoverable.
|
|
13
|
+
- agents/ is gitignored (personal to the deployment, unlike VectorStep's
|
|
14
|
+
git-controlled pipelines/) — every create/update/delete result notes the
|
|
15
|
+
files were written and reloaded, not that anything was committed.
|
|
16
|
+
- Secrets: agent.yaml uses '${VAR}' placeholders for anything sensitive —
|
|
17
|
+
pass them straight through, never resolve/inline a real value. The
|
|
18
|
+
gateway preserves them verbatim. This server never returns the operator
|
|
19
|
+
token, a provider API key, or any other config.yaml secret.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from ..errors import call_safe
|
|
23
|
+
from ..server import client, mcp
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
async def create_agent(name: str, agent_yaml: str, soul_md: str, overwrite: bool = False) -> dict:
|
|
28
|
+
"""Create a new agent from raw agent.yaml + soul.md text. `name` must
|
|
29
|
+
match the YAML's own 'name:' field. Fails with a 'collision' error if an
|
|
30
|
+
agent with that name already exists, unless overwrite=True. Validates
|
|
31
|
+
schema AND that model/model_fallbacks map to a configured provider and
|
|
32
|
+
tools: map to configured MCP servers (list_providers/list_mcp_servers
|
|
33
|
+
show what's available) — on success the files are written and the
|
|
34
|
+
gateway reloads, but agents/ is gitignored so there is nothing to commit.
|
|
35
|
+
Consider calling validate_agent first.
|
|
36
|
+
|
|
37
|
+
The `version` this agent is assigned (a content hash over its full
|
|
38
|
+
config — see get_agent) becomes the first entry in its calibration
|
|
39
|
+
history on the VectorStep side. There's no prior history to reset for a
|
|
40
|
+
brand-new agent, unlike update_agent below."""
|
|
41
|
+
return await call_safe(client.post("/agents", json={
|
|
42
|
+
"name": name, "agent_yaml": agent_yaml, "soul_md": soul_md, "overwrite": overwrite,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool()
|
|
47
|
+
async def update_agent(name: str, agent_yaml: str | None = None, soul_md: str | None = None) -> dict:
|
|
48
|
+
"""Update an existing agent (404 if 'name' doesn't exist yet). Pass only
|
|
49
|
+
agent_yaml, only soul_md, or both — an omitted field is left as-is. If
|
|
50
|
+
agent_yaml is given, its own 'name:' field must still match `name`;
|
|
51
|
+
renaming an agent is a delete_agent + create_agent, not an update.
|
|
52
|
+
Written and reloaded on success; agents/ is gitignored so there's
|
|
53
|
+
nothing to commit.
|
|
54
|
+
|
|
55
|
+
IMPORTANT — this changes more than the gateway: editing agent_yaml or
|
|
56
|
+
soul_md changes this agent's `version` (a content hash over its full
|
|
57
|
+
config), and VectorStep uses that version to scope calibration buckets. That
|
|
58
|
+
means this call starts a NEW calibration history in VectorStep for every step
|
|
59
|
+
that uses this agent — outcomes recorded under the old version stop
|
|
60
|
+
counting as evidence for the new one. If a step using this agent had
|
|
61
|
+
`calibration: {enforce: true}` and was passing (a "validated" bucket),
|
|
62
|
+
it will fall back to its `on_uncalibrated` behaviour (e.g. escalating
|
|
63
|
+
every run) until enough new labelled runs land under the new version —
|
|
64
|
+
VectorStep's default threshold is 20. This is not a bug on either side; it's
|
|
65
|
+
the correct, deliberate consequence of changing what the agent actually
|
|
66
|
+
does. Point whoever's asking "why did this step start escalating
|
|
67
|
+
everything" at VectorStep's get_agent_versions(name) tool or its
|
|
68
|
+
explain("prompt-versions") doc topic to see the history this creates."""
|
|
69
|
+
body: dict = {}
|
|
70
|
+
if agent_yaml is not None:
|
|
71
|
+
body["agent_yaml"] = agent_yaml
|
|
72
|
+
if soul_md is not None:
|
|
73
|
+
body["soul_md"] = soul_md
|
|
74
|
+
return await call_safe(client.put(f"/agents/{name}", json=body))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@mcp.tool()
|
|
78
|
+
async def delete_agent(name: str, confirm: bool = False) -> dict:
|
|
79
|
+
"""Permanently delete an agent's agent.yaml + soul.md. Destructive —
|
|
80
|
+
requires confirm=True or nothing is sent to the gateway at all (a
|
|
81
|
+
validation error is returned instead). On success returns the deleted
|
|
82
|
+
agent_yaml/soul_md in the result so the deletion is auditable and
|
|
83
|
+
recoverable by hand."""
|
|
84
|
+
if not confirm:
|
|
85
|
+
return {"error": {"type": "validation",
|
|
86
|
+
"message": "delete_agent requires confirm=True — refusing to delete without it.",
|
|
87
|
+
"detail": {"name": name}}}
|
|
88
|
+
return await call_safe(client.delete(f"/agents/{name}"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
async def reload() -> dict:
|
|
93
|
+
"""Re-read all agent.yaml/soul.md files from disk without restarting the
|
|
94
|
+
gateway. Usually unnecessary — create_agent/update_agent/delete_agent
|
|
95
|
+
already reload as part of their atomic write — but exposed for the rare
|
|
96
|
+
case an agent directory was edited outside these tools (e.g. directly on
|
|
97
|
+
the host) and you want the gateway to pick it up."""
|
|
98
|
+
return await call_safe(client.post("/reload"))
|