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.
Files changed (24) hide show
  1. vectorstep_gateway_mcp-0.1.0/LICENSE +21 -0
  2. vectorstep_gateway_mcp-0.1.0/PKG-INFO +85 -0
  3. vectorstep_gateway_mcp-0.1.0/README.md +54 -0
  4. vectorstep_gateway_mcp-0.1.0/pyproject.toml +55 -0
  5. vectorstep_gateway_mcp-0.1.0/setup.cfg +4 -0
  6. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/__init__.py +0 -0
  7. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/__main__.py +13 -0
  8. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/client.py +75 -0
  9. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/errors.py +76 -0
  10. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/server.py +9 -0
  11. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/tools/__init__.py +1 -0
  12. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/tools/read.py +86 -0
  13. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp/tools/write.py +98 -0
  14. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/PKG-INFO +85 -0
  15. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/SOURCES.txt +22 -0
  16. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/dependency_links.txt +1 -0
  17. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/entry_points.txt +2 -0
  18. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/requires.txt +7 -0
  19. vectorstep_gateway_mcp-0.1.0/src/vectorstep_gateway_mcp.egg-info/top_level.txt +1 -0
  20. vectorstep_gateway_mcp-0.1.0/tests/test_client.py +142 -0
  21. vectorstep_gateway_mcp-0.1.0/tests/test_e2e.py +179 -0
  22. vectorstep_gateway_mcp-0.1.0/tests/test_stub.py +5 -0
  23. vectorstep_gateway_mcp-0.1.0/tests/test_tools_read.py +189 -0
  24. 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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"))