swarmv-mcp 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.env
|
|
4
|
+
.dev.vars
|
|
5
|
+
.DS_Store
|
|
6
|
+
.backup/
|
|
7
|
+
# Secrets and local copies of them
|
|
8
|
+
*.bak
|
|
9
|
+
.env.*
|
|
10
|
+
!.env.example
|
|
11
|
+
# Build output and local state
|
|
12
|
+
.wrangler/
|
|
13
|
+
target/
|
|
14
|
+
crates/edgevec-wasm/pkg/
|
|
15
|
+
.next/
|
|
16
|
+
.vercel/
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.pyc
|
|
19
|
+
# Load test reports
|
|
20
|
+
load-test-*.json
|
|
21
|
+
.venv/
|
|
22
|
+
venv/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
# Separate repository (has its own .git)
|
|
26
|
+
edgevec/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: swarmv-mcp
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: MCP server for SwarmV: persistent vector memory for AI agents, limited to what the user may access.
|
|
5
|
+
Project-URL: Homepage, https://edgevec.pages.dev
|
|
6
|
+
Project-URL: Documentation, https://edgevec.pages.dev/docs
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: agents,ai,mcp,memory,swarmv,vector
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: mcp<2,>=1.1.2
|
|
12
|
+
Requires-Dist: swarmv>=1.2.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# swarmv-mcp
|
|
16
|
+
|
|
17
|
+
MCP server for [SwarmV](https://edgevec.pages.dev): gives an AI agent persistent vector memory, limited to what the signed-in user is allowed to see and edit.
|
|
18
|
+
|
|
19
|
+
Tools:
|
|
20
|
+
|
|
21
|
+
- `read_from_memory` returns the closest records (id, distance, metadata) from the containers the user can view.
|
|
22
|
+
- `write_to_memory` stores a note in a container the user can edit (put the text in `metadata.text`).
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install swarmv-mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add it to your MCP client's configuration (Claude Desktop, Cursor and others):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"swarmv": {
|
|
36
|
+
"command": "swarmv-mcp",
|
|
37
|
+
"env": {
|
|
38
|
+
"SWARMV_TOKEN_URL": "https://your-backend.example.com/swarmv-token",
|
|
39
|
+
"SWARMV_TOKEN_URL_AUTH": "<the signed-in user's session credential>"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or without installing first: `"command": "uvx", "args": ["swarmv-mcp"]`.
|
|
47
|
+
|
|
48
|
+
## Credentials
|
|
49
|
+
|
|
50
|
+
Pick one:
|
|
51
|
+
|
|
52
|
+
| Variables | Use |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `SWARMV_TOKEN_URL` (+ optional `SWARMV_TOKEN_URL_AUTH`) | Recommended. Your backend returns `{ "token": "..." }`, a short-lived user token it minted with your API key. |
|
|
55
|
+
| `SWARMV_USER_TOKEN` | A user token you already have (expires). |
|
|
56
|
+
| `SWARMV_API_KEY` + `SWARMV_USER_ID=user:<id>` | Local development only. Never ship your API key to end users. |
|
|
57
|
+
|
|
58
|
+
Optional: `SWARMV_DIM` (vector dimensions, default 768), `SWARMV_BASE_URL`.
|
|
59
|
+
|
|
60
|
+
Full documentation: https://edgevec.pages.dev/docs
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# swarmv-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [SwarmV](https://edgevec.pages.dev): gives an AI agent persistent vector memory, limited to what the signed-in user is allowed to see and edit.
|
|
4
|
+
|
|
5
|
+
Tools:
|
|
6
|
+
|
|
7
|
+
- `read_from_memory` returns the closest records (id, distance, metadata) from the containers the user can view.
|
|
8
|
+
- `write_to_memory` stores a note in a container the user can edit (put the text in `metadata.text`).
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install swarmv-mcp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Add it to your MCP client's configuration (Claude Desktop, Cursor and others):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"swarmv": {
|
|
22
|
+
"command": "swarmv-mcp",
|
|
23
|
+
"env": {
|
|
24
|
+
"SWARMV_TOKEN_URL": "https://your-backend.example.com/swarmv-token",
|
|
25
|
+
"SWARMV_TOKEN_URL_AUTH": "<the signed-in user's session credential>"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or without installing first: `"command": "uvx", "args": ["swarmv-mcp"]`.
|
|
33
|
+
|
|
34
|
+
## Credentials
|
|
35
|
+
|
|
36
|
+
Pick one:
|
|
37
|
+
|
|
38
|
+
| Variables | Use |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `SWARMV_TOKEN_URL` (+ optional `SWARMV_TOKEN_URL_AUTH`) | Recommended. Your backend returns `{ "token": "..." }`, a short-lived user token it minted with your API key. |
|
|
41
|
+
| `SWARMV_USER_TOKEN` | A user token you already have (expires). |
|
|
42
|
+
| `SWARMV_API_KEY` + `SWARMV_USER_ID=user:<id>` | Local development only. Never ship your API key to end users. |
|
|
43
|
+
|
|
44
|
+
Optional: `SWARMV_DIM` (vector dimensions, default 768), `SWARMV_BASE_URL`.
|
|
45
|
+
|
|
46
|
+
Full documentation: https://edgevec.pages.dev/docs
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "swarmv-mcp"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "MCP server for SwarmV: persistent vector memory for AI agents, limited to what the user may access."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["mcp", "ai", "agents", "memory", "vector", "swarmv"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp>=1.1.2,<2",
|
|
15
|
+
"swarmv>=1.2.0",
|
|
16
|
+
"httpx>=0.27"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://edgevec.pages.dev"
|
|
21
|
+
Documentation = "https://edgevec.pages.dev/docs"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.sdist]
|
|
24
|
+
include = ["src/swarmv_mcp", "README.md", "pyproject.toml"]
|
|
25
|
+
exclude = ["**/__pycache__"]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/swarmv_mcp"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
swarmv-mcp = "swarmv_mcp.server:main"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP Server implementation for SwarmV in Python."""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""SwarmV MCP server (Python).
|
|
2
|
+
|
|
3
|
+
The agent always acts as ONE user, fixed by the credentials below and never chosen by the model.
|
|
4
|
+
|
|
5
|
+
SWARMV_TOKEN_URL (recommended) Your backend endpoint returning {"token": "<user token>"} for the
|
|
6
|
+
signed-in user. Called with POST; SWARMV_TOKEN_URL_AUTH, if set, is sent as a Bearer
|
|
7
|
+
credential. Tokens are refreshed automatically when they expire.
|
|
8
|
+
SWARMV_USER_TOKEN A fixed user token from generate_user_token(). Expires within an hour; for testing.
|
|
9
|
+
SWARMV_API_KEY + Local development only: the server mints its own tokens for SWARMV_USER_ID.
|
|
10
|
+
SWARMV_USER_ID This keeps a backend key on the machine, so don't use it on shared or production hosts.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
from mcp.server import Server
|
|
21
|
+
from mcp.server.stdio import stdio_server
|
|
22
|
+
from mcp.types import TextContent, Tool
|
|
23
|
+
from swarmv import AsyncSwarmV
|
|
24
|
+
|
|
25
|
+
server = Server("swarmv-mcp-python")
|
|
26
|
+
|
|
27
|
+
BASE_URL = os.environ.get("SWARMV_BASE_URL", "https://edgevec-edge.lorencogonzaga.workers.dev")
|
|
28
|
+
DIM = int(os.environ.get("SWARMV_DIM", "768"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fail(msg: str) -> None:
|
|
32
|
+
print(msg, file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_client():
|
|
37
|
+
token_url = os.environ.get("SWARMV_TOKEN_URL")
|
|
38
|
+
user_token = os.environ.get("SWARMV_USER_TOKEN")
|
|
39
|
+
api_key = os.environ.get("SWARMV_API_KEY")
|
|
40
|
+
user_id = os.environ.get("SWARMV_USER_ID")
|
|
41
|
+
|
|
42
|
+
if token_url:
|
|
43
|
+
async def provider() -> str:
|
|
44
|
+
headers = {}
|
|
45
|
+
if os.environ.get("SWARMV_TOKEN_URL_AUTH"):
|
|
46
|
+
headers["Authorization"] = f"Bearer {os.environ['SWARMV_TOKEN_URL_AUTH']}"
|
|
47
|
+
async with httpx.AsyncClient(timeout=10) as http:
|
|
48
|
+
res = await http.post(token_url, headers=headers)
|
|
49
|
+
res.raise_for_status()
|
|
50
|
+
token = res.json().get("token")
|
|
51
|
+
if not token:
|
|
52
|
+
raise RuntimeError("Token endpoint response has no 'token' field")
|
|
53
|
+
return token
|
|
54
|
+
|
|
55
|
+
return AsyncSwarmV(token_provider=provider, base_url=BASE_URL), "token-url"
|
|
56
|
+
|
|
57
|
+
if user_token:
|
|
58
|
+
return AsyncSwarmV(token=user_token, base_url=BASE_URL), "user-token"
|
|
59
|
+
|
|
60
|
+
if api_key:
|
|
61
|
+
if not user_id or not user_id.startswith("user:"):
|
|
62
|
+
_fail(
|
|
63
|
+
"SWARMV_API_KEY alone is not allowed: the agent would act with backend-wide access.\n"
|
|
64
|
+
"Set SWARMV_TOKEN_URL (recommended), or SWARMV_USER_TOKEN, or add SWARMV_USER_ID=user:<id> for local development."
|
|
65
|
+
)
|
|
66
|
+
admin = AsyncSwarmV(api_key=api_key, base_url=BASE_URL)
|
|
67
|
+
|
|
68
|
+
async def provider() -> str:
|
|
69
|
+
return await admin.generate_user_token(user_id, 900, scopes=["vector:read", "vector:write"])
|
|
70
|
+
|
|
71
|
+
print(f"SwarmV MCP: minting tokens locally for {user_id}. Use SWARMV_TOKEN_URL outside local development.", file=sys.stderr)
|
|
72
|
+
return AsyncSwarmV(token_provider=provider, base_url=BASE_URL), "local-key"
|
|
73
|
+
|
|
74
|
+
_fail("No SwarmV credentials. Set SWARMV_TOKEN_URL (recommended), SWARMV_USER_TOKEN, or SWARMV_API_KEY with SWARMV_USER_ID.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
memory, MODE = _build_client()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _pad(v):
|
|
81
|
+
out = [float(x) for x in v[:DIM]]
|
|
82
|
+
out.extend([0.0] * (DIM - len(out)))
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@server.list_tools()
|
|
87
|
+
async def handle_list_tools() -> list[Tool]:
|
|
88
|
+
return [
|
|
89
|
+
Tool(
|
|
90
|
+
name="read_from_memory",
|
|
91
|
+
description="Search SwarmV memory. Returns the closest records with their id, distance (lower is closer) and stored metadata, such as the note text. Includes notes written moments ago. Results are limited to the containers the connected user can view.",
|
|
92
|
+
inputSchema={
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"query_vector": {"type": "array", "items": {"type": "number"}, "description": f"Dense embedding; padded/truncated to {DIM} dimensions."},
|
|
96
|
+
"top_k": {"type": "number", "description": "Number of results (1-100, default 10)."},
|
|
97
|
+
},
|
|
98
|
+
"required": ["query_vector"],
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
Tool(
|
|
102
|
+
name="write_to_memory",
|
|
103
|
+
description="Write a note to SwarmV memory in a container the connected user can edit. Put the note itself in metadata.text so it comes back from read_from_memory.",
|
|
104
|
+
inputSchema={
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"id": {"type": "string", "description": "Numeric string id (unsigned 32-bit integer)."},
|
|
108
|
+
"vector": {"type": "array", "items": {"type": "number"}, "description": f"Dense embedding; padded/truncated to {DIM} dimensions."},
|
|
109
|
+
"container_id": {"type": "string", "description": "Target container, e.g. 'container:apollo'. The user must be an editor of it."},
|
|
110
|
+
"metadata": {"type": "object", "description": "Flat metadata (string, number or boolean values; up to 8 KB), e.g. {text: 'the note'}. Values come back as strings."},
|
|
111
|
+
},
|
|
112
|
+
"required": ["id", "vector", "container_id"],
|
|
113
|
+
},
|
|
114
|
+
),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@server.call_tool()
|
|
119
|
+
async def handle_call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
120
|
+
try:
|
|
121
|
+
if name == "read_from_memory":
|
|
122
|
+
top_k = int(arguments.get("top_k", 10))
|
|
123
|
+
if not 1 <= top_k <= 100:
|
|
124
|
+
raise ValueError("top_k must be between 1 and 100")
|
|
125
|
+
# No auth_user_id: the user is fixed by the credential, so a prompt can't change who we search as.
|
|
126
|
+
# Agents often read what they (or another agent) just wrote: always include unmerged writes.
|
|
127
|
+
results = await memory.search(query=_pad(arguments["query_vector"]), top_k=top_k, fresh=True)
|
|
128
|
+
payload = [{"id": r.id, "distance": r.distance, "metadata": r.metadata} for r in results]
|
|
129
|
+
return [TextContent(type="text", text=json.dumps(payload, indent=2))]
|
|
130
|
+
|
|
131
|
+
if name == "write_to_memory":
|
|
132
|
+
id_ = str(arguments["id"])
|
|
133
|
+
if not re.fullmatch(r"\d+", id_):
|
|
134
|
+
raise ValueError("id must be a numeric string (unsigned 32-bit integer)")
|
|
135
|
+
container_id = arguments.get("container_id", "")
|
|
136
|
+
if not str(container_id).startswith("container:"):
|
|
137
|
+
raise ValueError("container_id must look like 'container:<name>'")
|
|
138
|
+
metadata = dict(arguments.get("metadata") or {})
|
|
139
|
+
metadata["container_id"] = container_id
|
|
140
|
+
# Write immediately (not buffered) so permission errors are reported back to the model.
|
|
141
|
+
await memory.insert_many([{"id": id_, "vector": _pad(arguments["vector"]), "metadata": metadata}])
|
|
142
|
+
return [TextContent(type="text", text=f"Stored {id_} in {container_id}.")]
|
|
143
|
+
|
|
144
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
return [TextContent(type="text", text=f"Error executing {name}: {e}")]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def run_server():
|
|
151
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
152
|
+
print(f"SwarmV MCP Server (Python) running on stdio (credentials: {MODE})", file=sys.stderr)
|
|
153
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
asyncio.run(run_server())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|