agent-utils-swarm 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.
- agent_utils_swarm-0.1.0/.github/workflows/release.yml +40 -0
- agent_utils_swarm-0.1.0/.gitignore +7 -0
- agent_utils_swarm-0.1.0/LICENSE +21 -0
- agent_utils_swarm-0.1.0/PKG-INFO +130 -0
- agent_utils_swarm-0.1.0/README.md +112 -0
- agent_utils_swarm-0.1.0/llms-install.md +103 -0
- agent_utils_swarm-0.1.0/pyproject.toml +28 -0
- agent_utils_swarm-0.1.0/src/agent_utils/swarm/__init__.py +287 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- name: Check tag matches version
|
|
16
|
+
run: |
|
|
17
|
+
version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
18
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
19
|
+
echo "tag $GITHUB_REF_NAME does not match pyproject version $version"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
- run: python -m pip install build
|
|
23
|
+
- run: python -m build
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Cortex LLC
|
|
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,130 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-utils-swarm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Thin client for the agent_utils Swarm MCP server - memory, locks, task board, pub/sub, idempotency and webhook inboxes for AI agents. One account spans both agent_utils servers.
|
|
5
|
+
Project-URL: Homepage, https://agent-utils.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/agent-utils/agent-utils-swarm-py
|
|
7
|
+
Author-email: AI Cortex LLC <support@agent-utils.ai>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent-memory,agent-utils,ai-agents,claude,distributed-locks,idempotency,mcp,model-context-protocol,multi-agent,pubsub,task-queue,webhooks
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# agent-utils-swarm
|
|
20
|
+
|
|
21
|
+
Thin Python client for the **agent_utils Swarm** server - stateful coordination for AI agents over MCP: memory, locks, task board, pub/sub, idempotency and webhook inboxes.
|
|
22
|
+
|
|
23
|
+
This is a client for a hosted commercial service ([agent-utils.ai](https://agent-utils.ai)), not the service itself. Zero dependencies (standard library only). Install name `agent-utils-swarm`; import `agent_utils.swarm` (a namespace package).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install agent-utils-swarm
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import json, os
|
|
35
|
+
from agent_utils.swarm import AgentUtilsSwarm
|
|
36
|
+
|
|
37
|
+
# Any secret works as a token; the first call auto-provisions a free trial.
|
|
38
|
+
swarm = AgentUtilsSwarm(token=os.environ["AGENT_UTILS_TOKEN"])
|
|
39
|
+
|
|
40
|
+
swarm.put_memory("project", "status", json.dumps({"phase": "review"}))
|
|
41
|
+
|
|
42
|
+
# Hand work out through the task board; any agent can claim it atomically.
|
|
43
|
+
swarm.create_task(board="launch", title="Write release notes")
|
|
44
|
+
claimed = swarm.claim_task(board="launch", agent_name="writer")
|
|
45
|
+
|
|
46
|
+
# Tell the rest of the swarm, and wait (long-poll) for replies.
|
|
47
|
+
swarm.publish(topic="launch", body="release notes in progress", sender="writer")
|
|
48
|
+
news = swarm.read_topic(topics=["launch"], wait_seconds=20)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
**Memory** - persistent key-value memory
|
|
54
|
+
- `get_memory(namespace, key)`
|
|
55
|
+
- `put_memory(namespace, key, value_json, months?, tags?, priority?)`
|
|
56
|
+
- `replace_memory(namespace, key, value_json, priority?, summary?, tags_to_add?, tags_to_delete?)`
|
|
57
|
+
- `extend_memory_ttl(namespace, key, months)`
|
|
58
|
+
- `delete_memory(namespace, key)`
|
|
59
|
+
- `list_keys(namespace)`
|
|
60
|
+
- `list_namespaces()`
|
|
61
|
+
- `search_by_tag(tag)`
|
|
62
|
+
- `top_memories(namespace, n)`
|
|
63
|
+
|
|
64
|
+
**Mutex** - distributed locks
|
|
65
|
+
- `acquire_lock(resource_id, agent_name, ttl_seconds?, reason?)`
|
|
66
|
+
- `release_lock(resource_id, kill_token)`
|
|
67
|
+
- `extend_lock(resource_id, kill_token, additional_ttl_seconds)`
|
|
68
|
+
- `check_lock(resource_id)`
|
|
69
|
+
- `list_active_locks()`
|
|
70
|
+
|
|
71
|
+
**Task board** - shared work queue with atomic claims, leases and dependencies
|
|
72
|
+
- `create_task(board, title, description?, priority?, depends_on?, tags?, memory?, created_by?)`
|
|
73
|
+
- `list_tasks(board, status?, owner?, tag?, ready_only?, limit?)`
|
|
74
|
+
- `get_task(board, task_id)`
|
|
75
|
+
- `claim_task(board, agent_name, task_id?, tag?, lease_seconds?)`
|
|
76
|
+
- `complete_task(board, task_id, agent_name, result?, memory?)`
|
|
77
|
+
- `release_task(board, task_id, agent_name)`
|
|
78
|
+
- `update_task(board, task_id, ...)`
|
|
79
|
+
- `delete_task(board, task_id)`
|
|
80
|
+
- `list_boards()`
|
|
81
|
+
- `renew_board(board, months?)`
|
|
82
|
+
- `delete_board(board)`
|
|
83
|
+
|
|
84
|
+
**Pub/sub** - topics agents publish to and long-poll
|
|
85
|
+
- `publish(topic, body, ttl_seconds?, sender?)`
|
|
86
|
+
- `read_topic(topics, after?, limit?, wait_seconds?)`
|
|
87
|
+
- `renew_message(topic, seq, ttl_seconds?)`
|
|
88
|
+
- `list_topics()`
|
|
89
|
+
- `delete_topic(topic, delete_token)`
|
|
90
|
+
|
|
91
|
+
**Idempotency** - exactly-once operations
|
|
92
|
+
- `acquire_key(key, operation, agent_name)`
|
|
93
|
+
- `complete_key / fail_key / check_key`
|
|
94
|
+
- `create_operation_rule / list_operation_rules / delete_operation_rule / cleanup_operation_rules`
|
|
95
|
+
|
|
96
|
+
**Webhook inbox** - temporary webhook receivers
|
|
97
|
+
- `create_inbox(...)`
|
|
98
|
+
- `poll_inbox(...)`
|
|
99
|
+
- `list_inboxes()`
|
|
100
|
+
- `delete_inbox(...)`
|
|
101
|
+
- `configure_inbox_settings(...)`
|
|
102
|
+
- `cleanup_expired()`
|
|
103
|
+
|
|
104
|
+
**Account** (on both servers) - `get_account`, `list_api_keys`, `create_api_key`, `revoke_api_key`, `list_payments`, `settle_balance`, `submit_feature_request`, `submit_exit_feedback`, `delete_account`
|
|
105
|
+
|
|
106
|
+
- `call(name, args)` - call any tool by name
|
|
107
|
+
|
|
108
|
+
Python methods use the tool names; TypeScript methods are their camelCase forms (`claim_task` -> `claimTask`). Memory has typed parameters; the other services take keyword arguments (Python) or an args object (TypeScript) that are passed straight through as the tool's arguments.
|
|
109
|
+
|
|
110
|
+
Every response carries a `_meta` envelope with `account_status`, `op_cost`, and
|
|
111
|
+
`credits_remaining`. The canonical, always-current reference is the server's
|
|
112
|
+
[llms.txt](https://mcp.agent-utils.ai/llms.txt) (per service: `/docs/<service>`).
|
|
113
|
+
|
|
114
|
+
Endpoint: `https://mcp.agent-utils.ai/swarm` (staging: `STAGING_ENDPOINT`, `https://mcp-staging.agent-utils.ai/swarm`).
|
|
115
|
+
|
|
116
|
+
## One token, both servers
|
|
117
|
+
|
|
118
|
+
agent_utils is two MCP servers on one account: **swarm** (this one) and **extensions** (math, units). The same token works on both and draws from one credit pool. See the [agent-utils hub](https://github.com/agent-utils/agent-utils) for the full platform, and the TypeScript client [`@agent-utils/swarm`](https://github.com/agent-utils/agent-utils-swarm-ts).
|
|
119
|
+
|
|
120
|
+
## Pricing
|
|
121
|
+
|
|
122
|
+
1 credit = $0.00001. Trial: 1,000 credits, no card. Activation: $5 -> 500,000-credit limit. Terms: [mcp.agent-utils.ai/legal.txt](https://mcp.agent-utils.ai/legal.txt).
|
|
123
|
+
|
|
124
|
+
## Contact
|
|
125
|
+
|
|
126
|
+
Questions, bugs or account issues: support@agent-utils.ai
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT. The hosted agent_utils service is a separate commercial product and is not open-source.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# agent-utils-swarm
|
|
2
|
+
|
|
3
|
+
Thin Python client for the **agent_utils Swarm** server - stateful coordination for AI agents over MCP: memory, locks, task board, pub/sub, idempotency and webhook inboxes.
|
|
4
|
+
|
|
5
|
+
This is a client for a hosted commercial service ([agent-utils.ai](https://agent-utils.ai)), not the service itself. Zero dependencies (standard library only). Install name `agent-utils-swarm`; import `agent_utils.swarm` (a namespace package).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install agent-utils-swarm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import json, os
|
|
17
|
+
from agent_utils.swarm import AgentUtilsSwarm
|
|
18
|
+
|
|
19
|
+
# Any secret works as a token; the first call auto-provisions a free trial.
|
|
20
|
+
swarm = AgentUtilsSwarm(token=os.environ["AGENT_UTILS_TOKEN"])
|
|
21
|
+
|
|
22
|
+
swarm.put_memory("project", "status", json.dumps({"phase": "review"}))
|
|
23
|
+
|
|
24
|
+
# Hand work out through the task board; any agent can claim it atomically.
|
|
25
|
+
swarm.create_task(board="launch", title="Write release notes")
|
|
26
|
+
claimed = swarm.claim_task(board="launch", agent_name="writer")
|
|
27
|
+
|
|
28
|
+
# Tell the rest of the swarm, and wait (long-poll) for replies.
|
|
29
|
+
swarm.publish(topic="launch", body="release notes in progress", sender="writer")
|
|
30
|
+
news = swarm.read_topic(topics=["launch"], wait_seconds=20)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
**Memory** - persistent key-value memory
|
|
36
|
+
- `get_memory(namespace, key)`
|
|
37
|
+
- `put_memory(namespace, key, value_json, months?, tags?, priority?)`
|
|
38
|
+
- `replace_memory(namespace, key, value_json, priority?, summary?, tags_to_add?, tags_to_delete?)`
|
|
39
|
+
- `extend_memory_ttl(namespace, key, months)`
|
|
40
|
+
- `delete_memory(namespace, key)`
|
|
41
|
+
- `list_keys(namespace)`
|
|
42
|
+
- `list_namespaces()`
|
|
43
|
+
- `search_by_tag(tag)`
|
|
44
|
+
- `top_memories(namespace, n)`
|
|
45
|
+
|
|
46
|
+
**Mutex** - distributed locks
|
|
47
|
+
- `acquire_lock(resource_id, agent_name, ttl_seconds?, reason?)`
|
|
48
|
+
- `release_lock(resource_id, kill_token)`
|
|
49
|
+
- `extend_lock(resource_id, kill_token, additional_ttl_seconds)`
|
|
50
|
+
- `check_lock(resource_id)`
|
|
51
|
+
- `list_active_locks()`
|
|
52
|
+
|
|
53
|
+
**Task board** - shared work queue with atomic claims, leases and dependencies
|
|
54
|
+
- `create_task(board, title, description?, priority?, depends_on?, tags?, memory?, created_by?)`
|
|
55
|
+
- `list_tasks(board, status?, owner?, tag?, ready_only?, limit?)`
|
|
56
|
+
- `get_task(board, task_id)`
|
|
57
|
+
- `claim_task(board, agent_name, task_id?, tag?, lease_seconds?)`
|
|
58
|
+
- `complete_task(board, task_id, agent_name, result?, memory?)`
|
|
59
|
+
- `release_task(board, task_id, agent_name)`
|
|
60
|
+
- `update_task(board, task_id, ...)`
|
|
61
|
+
- `delete_task(board, task_id)`
|
|
62
|
+
- `list_boards()`
|
|
63
|
+
- `renew_board(board, months?)`
|
|
64
|
+
- `delete_board(board)`
|
|
65
|
+
|
|
66
|
+
**Pub/sub** - topics agents publish to and long-poll
|
|
67
|
+
- `publish(topic, body, ttl_seconds?, sender?)`
|
|
68
|
+
- `read_topic(topics, after?, limit?, wait_seconds?)`
|
|
69
|
+
- `renew_message(topic, seq, ttl_seconds?)`
|
|
70
|
+
- `list_topics()`
|
|
71
|
+
- `delete_topic(topic, delete_token)`
|
|
72
|
+
|
|
73
|
+
**Idempotency** - exactly-once operations
|
|
74
|
+
- `acquire_key(key, operation, agent_name)`
|
|
75
|
+
- `complete_key / fail_key / check_key`
|
|
76
|
+
- `create_operation_rule / list_operation_rules / delete_operation_rule / cleanup_operation_rules`
|
|
77
|
+
|
|
78
|
+
**Webhook inbox** - temporary webhook receivers
|
|
79
|
+
- `create_inbox(...)`
|
|
80
|
+
- `poll_inbox(...)`
|
|
81
|
+
- `list_inboxes()`
|
|
82
|
+
- `delete_inbox(...)`
|
|
83
|
+
- `configure_inbox_settings(...)`
|
|
84
|
+
- `cleanup_expired()`
|
|
85
|
+
|
|
86
|
+
**Account** (on both servers) - `get_account`, `list_api_keys`, `create_api_key`, `revoke_api_key`, `list_payments`, `settle_balance`, `submit_feature_request`, `submit_exit_feedback`, `delete_account`
|
|
87
|
+
|
|
88
|
+
- `call(name, args)` - call any tool by name
|
|
89
|
+
|
|
90
|
+
Python methods use the tool names; TypeScript methods are their camelCase forms (`claim_task` -> `claimTask`). Memory has typed parameters; the other services take keyword arguments (Python) or an args object (TypeScript) that are passed straight through as the tool's arguments.
|
|
91
|
+
|
|
92
|
+
Every response carries a `_meta` envelope with `account_status`, `op_cost`, and
|
|
93
|
+
`credits_remaining`. The canonical, always-current reference is the server's
|
|
94
|
+
[llms.txt](https://mcp.agent-utils.ai/llms.txt) (per service: `/docs/<service>`).
|
|
95
|
+
|
|
96
|
+
Endpoint: `https://mcp.agent-utils.ai/swarm` (staging: `STAGING_ENDPOINT`, `https://mcp-staging.agent-utils.ai/swarm`).
|
|
97
|
+
|
|
98
|
+
## One token, both servers
|
|
99
|
+
|
|
100
|
+
agent_utils is two MCP servers on one account: **swarm** (this one) and **extensions** (math, units). The same token works on both and draws from one credit pool. See the [agent-utils hub](https://github.com/agent-utils/agent-utils) for the full platform, and the TypeScript client [`@agent-utils/swarm`](https://github.com/agent-utils/agent-utils-swarm-ts).
|
|
101
|
+
|
|
102
|
+
## Pricing
|
|
103
|
+
|
|
104
|
+
1 credit = $0.00001. Trial: 1,000 credits, no card. Activation: $5 -> 500,000-credit limit. Terms: [mcp.agent-utils.ai/legal.txt](https://mcp.agent-utils.ai/legal.txt).
|
|
105
|
+
|
|
106
|
+
## Contact
|
|
107
|
+
|
|
108
|
+
Questions, bugs or account issues: support@agent-utils.ai
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT. The hosted agent_utils service is a separate commercial product and is not open-source.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Install - instructions for an AI agent
|
|
2
|
+
|
|
3
|
+
**Nothing to clone, build, or run.** agent_utils Swarm is a *hosted* MCP server at
|
|
4
|
+
`https://mcp.agent-utils.ai/swarm`. Installing it means pointing your MCP client at that URL with a
|
|
5
|
+
Bearer token. There is no signup, no dashboard, no API-key request, and no payment card.
|
|
6
|
+
|
|
7
|
+
If you are looking for a package to launch as a local stdio server, stop - this is a remote
|
|
8
|
+
(Streamable HTTP) server. The npm/PyPI package below is an optional convenience client, not
|
|
9
|
+
the server.
|
|
10
|
+
|
|
11
|
+
## 1. Generate a token
|
|
12
|
+
|
|
13
|
+
The token *is* the account. Any sufficiently random secret works, and the first call
|
|
14
|
+
auto-provisions a free trial (1,000 credits).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
TOKEN=sk_$(openssl rand -hex 32)
|
|
18
|
+
echo "$TOKEN" # save this - losing it means losing the account and its data
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 2. Register the server
|
|
22
|
+
|
|
23
|
+
**Claude Code** - one command:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
claude mcp add --scope user --transport http agent-utils-swarm https://mcp.agent-utils.ai/swarm \
|
|
27
|
+
--header "Authorization: Bearer $TOKEN"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Cline / Cursor / Windsurf** - add to the MCP settings JSON:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"agent-utils-swarm": {
|
|
36
|
+
"url": "https://mcp.agent-utils.ai/swarm",
|
|
37
|
+
"headers": { "Authorization": "Bearer sk_REPLACE_WITH_YOUR_TOKEN" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 3. Verify before spending anything
|
|
44
|
+
|
|
45
|
+
`tools/list` needs **no authentication**, so reachability can be confirmed before the first
|
|
46
|
+
billed call:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -s -X POST https://mcp.agent-utils.ai/swarm \
|
|
50
|
+
-H 'Content-Type: application/json' \
|
|
51
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Expect 53 tools: memory, mutex, task board, pub/sub, idempotency and webhook inbox, plus the 9 shared account tools
|
|
55
|
+
(`get_account`, `create_api_key`, `list_api_keys`, `revoke_api_key`, `list_payments`,
|
|
56
|
+
`settle_balance`, `submit_feature_request`, `submit_exit_feedback`).
|
|
57
|
+
|
|
58
|
+
## 4. Confirm the account
|
|
59
|
+
|
|
60
|
+
`get_account` is free - it creates the trial account and returns its state:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl -s -X POST https://mcp.agent-utils.ai/swarm \
|
|
64
|
+
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
|
65
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_account","arguments":{}}}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A successful response means installation is complete.
|
|
69
|
+
|
|
70
|
+
## Optional: the client library
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install agent-utils-swarm
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See `README.md` in this repo for the API.
|
|
77
|
+
|
|
78
|
+
## One token, both servers
|
|
79
|
+
|
|
80
|
+
agent_utils is two MCP servers: **swarm** (`https://mcp.agent-utils.ai/swarm` - memory, mutex,
|
|
81
|
+
task board, pub/sub, idempotency, webhook inbox) and **extensions**
|
|
82
|
+
(`https://mcp.agent-utils.ai/extensions` - math, units). The same token works on
|
|
83
|
+
both and draws from a single credit pool. To add the other one, repeat step 2 with
|
|
84
|
+
`agent-utils-extensions`, `https://mcp.agent-utils.ai/extensions`, and **the same token**.
|
|
85
|
+
|
|
86
|
+
## Cost
|
|
87
|
+
|
|
88
|
+
1 credit = $0.00001. Trial: 1,000 credits, no card. A one-time $5 activation raises the limit
|
|
89
|
+
to 500,000 credits for the whole account. `get_account`, `settle_balance`, and
|
|
90
|
+
`submit_exit_feedback` are free. Terms: <https://mcp.agent-utils.ai/legal.txt>.
|
|
91
|
+
|
|
92
|
+
## Troubleshooting
|
|
93
|
+
|
|
94
|
+
- **HTTP 401** - the `Authorization` header is missing or malformed. It must be exactly
|
|
95
|
+
`Bearer <token>`.
|
|
96
|
+
- **HTTP 403 "error code: 1010"** - Cloudflare rejected your HTTP client's default
|
|
97
|
+
User-Agent (Python's `urllib` is blocked). Send any descriptive `User-Agent` header.
|
|
98
|
+
- **No tools listed / 404** - you are probably POSTing to the wrong path. The endpoint is
|
|
99
|
+
`/swarm`, not the domain root or `/mcp`.
|
|
100
|
+
- **"Key already exists" on a write** - services distinguish create from update. Check the
|
|
101
|
+
tool description returned by `tools/list`; it names the right tool.
|
|
102
|
+
- **Authoritative reference** - the server's own spec is always current:
|
|
103
|
+
<https://mcp.agent-utils.ai/llms.txt>.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-utils-swarm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Thin client for the agent_utils Swarm MCP server - memory, locks, task board, pub/sub, idempotency and webhook inboxes for AI agents. One account spans both agent_utils servers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "AI Cortex LLC", email = "support@agent-utils.ai" }]
|
|
13
|
+
keywords = ["mcp", "model-context-protocol", "ai-agents", "agent-memory", "distributed-locks", "task-queue", "pubsub", "idempotency", "webhooks", "multi-agent", "claude", "agent-utils"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://agent-utils.ai"
|
|
25
|
+
Repository = "https://github.com/agent-utils/agent-utils-swarm-py"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/agent_utils"]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Thin MCP-over-HTTP client for the hosted agent_utils Swarm server.
|
|
3
|
+
|
|
4
|
+
Swarm is the stateful half of agent_utils: memory, mutex, task board, pub/sub,
|
|
5
|
+
idempotency and webhook inbox, all on one endpoint.
|
|
6
|
+
|
|
7
|
+
This is a client, not the service - see https://agent-utils.ai.
|
|
8
|
+
Zero dependencies: standard library only.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
DEFAULT_ENDPOINT = "https://mcp.agent-utils.ai/swarm"
|
|
19
|
+
STAGING_ENDPOINT = "https://mcp-staging.agent-utils.ai/swarm"
|
|
20
|
+
USER_AGENT = "agent-utils-swarm-py/0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = ["AgentUtilsSwarm", "DEFAULT_ENDPOINT", "STAGING_ENDPOINT"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentUtilsSwarm:
|
|
26
|
+
"""Client for the agent_utils Swarm MCP server.
|
|
27
|
+
|
|
28
|
+
The same token works on both agent_utils servers (swarm and extensions) and
|
|
29
|
+
draws from one credit pool. Any secret works as a token - the first call
|
|
30
|
+
auto-provisions a free trial (no card). See https://agent-utils.ai.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, token: str, endpoint: str = DEFAULT_ENDPOINT) -> None:
|
|
34
|
+
self._token = token
|
|
35
|
+
self._endpoint = endpoint
|
|
36
|
+
self._id = 0
|
|
37
|
+
|
|
38
|
+
def call(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
|
|
39
|
+
"""Call any MCP tool by name and return its result."""
|
|
40
|
+
self._id += 1
|
|
41
|
+
payload = json.dumps(
|
|
42
|
+
{
|
|
43
|
+
"jsonrpc": "2.0",
|
|
44
|
+
"id": self._id,
|
|
45
|
+
"method": "tools/call",
|
|
46
|
+
"params": {"name": name, "arguments": arguments or {}},
|
|
47
|
+
}
|
|
48
|
+
).encode()
|
|
49
|
+
req = urllib.request.Request(
|
|
50
|
+
self._endpoint,
|
|
51
|
+
data=payload,
|
|
52
|
+
method="POST",
|
|
53
|
+
headers={
|
|
54
|
+
"Authorization": f"Bearer {self._token}",
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
# Cloudflare rejects urllib's default User-Agent (error 1010).
|
|
57
|
+
"User-Agent": USER_AGENT,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req) as resp:
|
|
62
|
+
body = json.loads(resp.read())
|
|
63
|
+
except urllib.error.HTTPError as exc: # pragma: no cover - network error path
|
|
64
|
+
raise RuntimeError(f"agent_utils HTTP {exc.code}: {exc.read().decode()}") from exc
|
|
65
|
+
if isinstance(body, dict) and body.get("error"):
|
|
66
|
+
raise RuntimeError(f"agent_utils: {body['error'].get('message', 'unknown error')}")
|
|
67
|
+
return body.get("result") if isinstance(body, dict) else body
|
|
68
|
+
|
|
69
|
+
# --- memory ---------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def get_memory(self, namespace: str, key: str) -> Any:
|
|
72
|
+
return self.call("get_memory", {"namespace": namespace, "key": key})
|
|
73
|
+
|
|
74
|
+
def put_memory(
|
|
75
|
+
self,
|
|
76
|
+
namespace: str,
|
|
77
|
+
key: str,
|
|
78
|
+
value_json: str,
|
|
79
|
+
months: int | None = None,
|
|
80
|
+
tags: list[str] | None = None,
|
|
81
|
+
priority: int | None = None,
|
|
82
|
+
) -> Any:
|
|
83
|
+
args: dict[str, Any] = {"namespace": namespace, "key": key, "value_json": value_json}
|
|
84
|
+
if months is not None:
|
|
85
|
+
args["months"] = months
|
|
86
|
+
if tags is not None:
|
|
87
|
+
args["tags"] = tags
|
|
88
|
+
if priority is not None:
|
|
89
|
+
args["priority"] = priority
|
|
90
|
+
return self.call("put_memory", args)
|
|
91
|
+
|
|
92
|
+
def replace_memory(
|
|
93
|
+
self,
|
|
94
|
+
namespace: str,
|
|
95
|
+
key: str,
|
|
96
|
+
value_json: str,
|
|
97
|
+
priority: int | None = None,
|
|
98
|
+
summary: str | None = None,
|
|
99
|
+
tags_to_add: list[str] | None = None,
|
|
100
|
+
tags_to_delete: list[str] | None = None,
|
|
101
|
+
) -> Any:
|
|
102
|
+
"""Update an existing entry.
|
|
103
|
+
|
|
104
|
+
Errors if the key does not exist - use :meth:`put_memory` to create.
|
|
105
|
+
Tags are managed explicitly; each tag operation is charged whether or
|
|
106
|
+
not the tag was already present.
|
|
107
|
+
"""
|
|
108
|
+
args: dict[str, Any] = {"namespace": namespace, "key": key, "value_json": value_json}
|
|
109
|
+
if priority is not None:
|
|
110
|
+
args["priority"] = priority
|
|
111
|
+
if summary is not None:
|
|
112
|
+
args["summary"] = summary
|
|
113
|
+
if tags_to_add is not None:
|
|
114
|
+
args["tags_to_add"] = tags_to_add
|
|
115
|
+
if tags_to_delete is not None:
|
|
116
|
+
args["tags_to_delete"] = tags_to_delete
|
|
117
|
+
return self.call("replace_memory", args)
|
|
118
|
+
|
|
119
|
+
def extend_memory_ttl(self, namespace: str, key: str, months: int) -> Any:
|
|
120
|
+
"""Add `months` to an existing entry's expiry.
|
|
121
|
+
|
|
122
|
+
Bills kb-months at the current stored size. Errors if the key does not
|
|
123
|
+
exist or has already expired.
|
|
124
|
+
"""
|
|
125
|
+
return self.call(
|
|
126
|
+
"extend_memory_ttl", {"namespace": namespace, "key": key, "months": months}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def delete_memory(self, namespace: str, key: str) -> Any:
|
|
130
|
+
return self.call("delete_memory", {"namespace": namespace, "key": key})
|
|
131
|
+
|
|
132
|
+
def list_keys(self, namespace: str) -> Any:
|
|
133
|
+
return self.call("list_keys", {"namespace": namespace})
|
|
134
|
+
|
|
135
|
+
def list_namespaces(self) -> Any:
|
|
136
|
+
return self.call("list_namespaces", {})
|
|
137
|
+
|
|
138
|
+
def search_by_tag(self, tag: str) -> Any:
|
|
139
|
+
return self.call("search_by_tag", {"tag": tag})
|
|
140
|
+
|
|
141
|
+
def top_memories(self, namespace: str, n: int) -> Any:
|
|
142
|
+
return self.call("top_memories", {"namespace": namespace, "n": n})
|
|
143
|
+
|
|
144
|
+
# --- mutex ----------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def acquire_lock(self, **kwargs: Any) -> Any:
|
|
147
|
+
return self.call("acquire_lock", kwargs)
|
|
148
|
+
|
|
149
|
+
def release_lock(self, **kwargs: Any) -> Any:
|
|
150
|
+
return self.call("release_lock", kwargs)
|
|
151
|
+
|
|
152
|
+
def extend_lock(self, **kwargs: Any) -> Any:
|
|
153
|
+
return self.call("extend_lock", kwargs)
|
|
154
|
+
|
|
155
|
+
def check_lock(self, **kwargs: Any) -> Any:
|
|
156
|
+
return self.call("check_lock", kwargs)
|
|
157
|
+
|
|
158
|
+
def list_active_locks(self, **kwargs: Any) -> Any:
|
|
159
|
+
return self.call("list_active_locks", kwargs)
|
|
160
|
+
|
|
161
|
+
# --- task board -----------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def create_task(self, **kwargs: Any) -> Any:
|
|
164
|
+
return self.call("create_task", kwargs)
|
|
165
|
+
|
|
166
|
+
def get_task(self, **kwargs: Any) -> Any:
|
|
167
|
+
return self.call("get_task", kwargs)
|
|
168
|
+
|
|
169
|
+
def list_tasks(self, **kwargs: Any) -> Any:
|
|
170
|
+
return self.call("list_tasks", kwargs)
|
|
171
|
+
|
|
172
|
+
def update_task(self, **kwargs: Any) -> Any:
|
|
173
|
+
return self.call("update_task", kwargs)
|
|
174
|
+
|
|
175
|
+
def claim_task(self, **kwargs: Any) -> Any:
|
|
176
|
+
return self.call("claim_task", kwargs)
|
|
177
|
+
|
|
178
|
+
def complete_task(self, **kwargs: Any) -> Any:
|
|
179
|
+
return self.call("complete_task", kwargs)
|
|
180
|
+
|
|
181
|
+
def release_task(self, **kwargs: Any) -> Any:
|
|
182
|
+
return self.call("release_task", kwargs)
|
|
183
|
+
|
|
184
|
+
def delete_task(self, **kwargs: Any) -> Any:
|
|
185
|
+
return self.call("delete_task", kwargs)
|
|
186
|
+
|
|
187
|
+
def list_boards(self, **kwargs: Any) -> Any:
|
|
188
|
+
return self.call("list_boards", kwargs)
|
|
189
|
+
|
|
190
|
+
def renew_board(self, **kwargs: Any) -> Any:
|
|
191
|
+
return self.call("renew_board", kwargs)
|
|
192
|
+
|
|
193
|
+
def delete_board(self, **kwargs: Any) -> Any:
|
|
194
|
+
return self.call("delete_board", kwargs)
|
|
195
|
+
|
|
196
|
+
# --- pub/sub --------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def publish(self, **kwargs: Any) -> Any:
|
|
199
|
+
return self.call("publish", kwargs)
|
|
200
|
+
|
|
201
|
+
def read_topic(self, **kwargs: Any) -> Any:
|
|
202
|
+
"""Read messages; pass `wait_seconds` (max 20) to long-poll."""
|
|
203
|
+
return self.call("read_topic", kwargs)
|
|
204
|
+
|
|
205
|
+
def renew_message(self, **kwargs: Any) -> Any:
|
|
206
|
+
return self.call("renew_message", kwargs)
|
|
207
|
+
|
|
208
|
+
def list_topics(self, **kwargs: Any) -> Any:
|
|
209
|
+
return self.call("list_topics", kwargs)
|
|
210
|
+
|
|
211
|
+
def delete_topic(self, **kwargs: Any) -> Any:
|
|
212
|
+
return self.call("delete_topic", kwargs)
|
|
213
|
+
|
|
214
|
+
# --- idempotency ----------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def list_operation_rules(self, **kwargs: Any) -> Any:
|
|
217
|
+
return self.call("list_operation_rules", kwargs)
|
|
218
|
+
|
|
219
|
+
def create_operation_rule(self, **kwargs: Any) -> Any:
|
|
220
|
+
return self.call("create_operation_rule", kwargs)
|
|
221
|
+
|
|
222
|
+
def delete_operation_rule(self, **kwargs: Any) -> Any:
|
|
223
|
+
return self.call("delete_operation_rule", kwargs)
|
|
224
|
+
|
|
225
|
+
def acquire_key(self, **kwargs: Any) -> Any:
|
|
226
|
+
return self.call("acquire_key", kwargs)
|
|
227
|
+
|
|
228
|
+
def complete_key(self, **kwargs: Any) -> Any:
|
|
229
|
+
return self.call("complete_key", kwargs)
|
|
230
|
+
|
|
231
|
+
def fail_key(self, **kwargs: Any) -> Any:
|
|
232
|
+
return self.call("fail_key", kwargs)
|
|
233
|
+
|
|
234
|
+
def check_key(self, **kwargs: Any) -> Any:
|
|
235
|
+
return self.call("check_key", kwargs)
|
|
236
|
+
|
|
237
|
+
def cleanup_operation_rules(self, **kwargs: Any) -> Any:
|
|
238
|
+
return self.call("cleanup_operation_rules", kwargs)
|
|
239
|
+
|
|
240
|
+
# --- webhook inbox --------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
def create_inbox(self, **kwargs: Any) -> Any:
|
|
243
|
+
return self.call("create_inbox", kwargs)
|
|
244
|
+
|
|
245
|
+
def poll_inbox(self, **kwargs: Any) -> Any:
|
|
246
|
+
return self.call("poll_inbox", kwargs)
|
|
247
|
+
|
|
248
|
+
def list_inboxes(self, **kwargs: Any) -> Any:
|
|
249
|
+
return self.call("list_inboxes", kwargs)
|
|
250
|
+
|
|
251
|
+
def delete_inbox(self, **kwargs: Any) -> Any:
|
|
252
|
+
return self.call("delete_inbox", kwargs)
|
|
253
|
+
|
|
254
|
+
def configure_inbox_settings(self, **kwargs: Any) -> Any:
|
|
255
|
+
return self.call("configure_inbox_settings", kwargs)
|
|
256
|
+
|
|
257
|
+
def cleanup_expired(self, **kwargs: Any) -> Any:
|
|
258
|
+
return self.call("cleanup_expired", kwargs)
|
|
259
|
+
|
|
260
|
+
# --- account (also on extensions) -----------------------------------------
|
|
261
|
+
|
|
262
|
+
def get_account(self) -> Any:
|
|
263
|
+
return self.call("get_account", {})
|
|
264
|
+
|
|
265
|
+
def list_api_keys(self, **kwargs: Any) -> Any:
|
|
266
|
+
return self.call("list_api_keys", kwargs)
|
|
267
|
+
|
|
268
|
+
def create_api_key(self, **kwargs: Any) -> Any:
|
|
269
|
+
return self.call("create_api_key", kwargs)
|
|
270
|
+
|
|
271
|
+
def revoke_api_key(self, **kwargs: Any) -> Any:
|
|
272
|
+
return self.call("revoke_api_key", kwargs)
|
|
273
|
+
|
|
274
|
+
def list_payments(self, **kwargs: Any) -> Any:
|
|
275
|
+
return self.call("list_payments", kwargs)
|
|
276
|
+
|
|
277
|
+
def settle_balance(self, **kwargs: Any) -> Any:
|
|
278
|
+
return self.call("settle_balance", kwargs)
|
|
279
|
+
|
|
280
|
+
def submit_feature_request(self, **kwargs: Any) -> Any:
|
|
281
|
+
return self.call("submit_feature_request", kwargs)
|
|
282
|
+
|
|
283
|
+
def submit_exit_feedback(self, **kwargs: Any) -> Any:
|
|
284
|
+
return self.call("submit_exit_feedback", kwargs)
|
|
285
|
+
|
|
286
|
+
def delete_account(self, **kwargs: Any) -> Any:
|
|
287
|
+
return self.call("delete_account", kwargs)
|