almega-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.
- almega_mcp-0.1.0/LICENSE +21 -0
- almega_mcp-0.1.0/PKG-INFO +223 -0
- almega_mcp-0.1.0/README.md +196 -0
- almega_mcp-0.1.0/almega_mcp.egg-info/PKG-INFO +223 -0
- almega_mcp-0.1.0/almega_mcp.egg-info/SOURCES.txt +10 -0
- almega_mcp-0.1.0/almega_mcp.egg-info/dependency_links.txt +1 -0
- almega_mcp-0.1.0/almega_mcp.egg-info/entry_points.txt +2 -0
- almega_mcp-0.1.0/almega_mcp.egg-info/requires.txt +3 -0
- almega_mcp-0.1.0/almega_mcp.egg-info/top_level.txt +1 -0
- almega_mcp-0.1.0/almega_mcp.py +601 -0
- almega_mcp-0.1.0/pyproject.toml +52 -0
- almega_mcp-0.1.0/setup.cfg +4 -0
almega_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Almega
|
|
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,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: almega-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A wallet & guardrail for AI agents: per-agent spending limits, allow-listed categories, 1-click human approval, and a full audit ledger, backed by Stripe Issuing.
|
|
5
|
+
Author: Almega
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://alemgaai.netlify.app
|
|
8
|
+
Project-URL: Repository, https://github.com/almega-ai/almega-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/almega-ai/almega-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,ai-agents,agent-infrastructure,payments,stripe,fintech,guardrails,wallet,human-in-the-loop
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
24
|
+
Requires-Dist: stripe>=11.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<!-- mcp-name: io.github.almega-ai/almega-mcp -->
|
|
29
|
+
|
|
30
|
+
# ๐งช Almega MCP โ the demonstrator
|
|
31
|
+
|
|
32
|
+
> A wallet & guardrail for AI agents, exposed as a Model Context Protocol
|
|
33
|
+
> (MCP) server. Drop it into Claude Desktop, the Claude Agent SDK, or any
|
|
34
|
+
> MCP-compatible client, and your agent has a wallet with hard limits, a
|
|
35
|
+
> human approval step, and a full ledger โ instantly.
|
|
36
|
+
>
|
|
37
|
+
> Ships with **two backends** out of the box:
|
|
38
|
+
>
|
|
39
|
+
> - `memory` (default): everything in-process. Zero setup.
|
|
40
|
+
> - `stripe`: real Stripe Issuing test-mode Cardholders + virtual Cards.
|
|
41
|
+
> No real money. You watch the dashboard light up live.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Tools the server exposes
|
|
46
|
+
|
|
47
|
+
| Tool | What it does |
|
|
48
|
+
|------|--------------|
|
|
49
|
+
| `open_wallet(agent_id, monthly_limit, allow, approve_above)` | Give an agent a wallet (and a real Stripe card if backend=stripe) |
|
|
50
|
+
| `pay(agent_id, merchant, amount, category)` | Agent tries to spend โ gets `APPROVED`, `BLOCKED`, or `AWAITING_YOU` |
|
|
51
|
+
| `approve_pending(transaction_id)` | Human says yes to a held transaction |
|
|
52
|
+
| `reject_pending(transaction_id, reason)` | Human says no |
|
|
53
|
+
| `get_wallet(agent_id)` | Current balance & rules |
|
|
54
|
+
| `list_transactions(agent_id?, status?, limit)` | View the ledger |
|
|
55
|
+
| `reset()` | Wipe the local index (Stripe entities are kept) |
|
|
56
|
+
|
|
57
|
+
Plus two resources:
|
|
58
|
+
|
|
59
|
+
- `almega://wallets` โ pretty-printed list of every wallet
|
|
60
|
+
- `almega://ledger` โ pretty-printed full ledger
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install -r requirements.txt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(Installs `mcp[cli]` and `stripe`. Python 3.10+ recommended.)
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Option A โ Memory backend (30-second demo)
|
|
75
|
+
|
|
76
|
+
No accounts, no env vars. Just run:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mcp dev almega_mcp.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That opens the MCP Inspector in your browser. Call `open_wallet`, `pay`,
|
|
83
|
+
`approve_pending` by hand and watch the ledger update.
|
|
84
|
+
|
|
85
|
+
Or run the scripted scenario:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python demo.py
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Option B โ Stripe Issuing test mode (5 minutes, still $0)
|
|
94
|
+
|
|
95
|
+
Now the wallet maps to a **real Stripe Cardholder + virtual Card** and every
|
|
96
|
+
`pay()` creates a real **test-mode authorization**. You can open the Stripe
|
|
97
|
+
dashboard and see Almega's ledger reflected on Stripe in real time.
|
|
98
|
+
|
|
99
|
+
### Setup
|
|
100
|
+
|
|
101
|
+
1. Create a free Stripe account: <https://dashboard.stripe.com/register>
|
|
102
|
+
2. Activate Issuing in test mode: <https://dashboard.stripe.com/test/issuing/overview>
|
|
103
|
+
(Stripe asks for some business info even in test โ fill it in. Nothing
|
|
104
|
+
leaves test mode until you flip "Activate live".)
|
|
105
|
+
3. Grab your **TEST** secret key: <https://dashboard.stripe.com/test/apikeys>
|
|
106
|
+
|
|
107
|
+
### Run
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export STRIPE_SECRET_KEY=sk_test_...
|
|
111
|
+
export ALMEGA_BACKEND=stripe
|
|
112
|
+
python stripe_demo.py
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Almega refuses to start if your key isn't `sk_test_...` โ there's no path
|
|
116
|
+
to accidentally hit live cards from this code.
|
|
117
|
+
|
|
118
|
+
### What you'll see in the dashboard
|
|
119
|
+
|
|
120
|
+
- **<https://dashboard.stripe.com/test/issuing/cards>**
|
|
121
|
+
one virtual card per agent, with the agent name on it.
|
|
122
|
+
- **<https://dashboard.stripe.com/test/issuing/authorizations>**
|
|
123
|
+
every `pay()` call as a real Stripe authorization, marked
|
|
124
|
+
*approved* or *declined* exactly the way Almega decided.
|
|
125
|
+
|
|
126
|
+
The wiring is intentionally direct: Almega decides locally, then mirrors
|
|
127
|
+
the decision onto Stripe. The next step (Phase 4) flips it: Stripe sends a
|
|
128
|
+
webhook to your server and Almega decides *during* the authorization. The
|
|
129
|
+
demo here is the synchronous half โ both halves expose the same MCP
|
|
130
|
+
surface to the agent.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Wire it into Claude Desktop
|
|
135
|
+
|
|
136
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` on
|
|
137
|
+
macOS (or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"mcpServers": {
|
|
142
|
+
"almega": {
|
|
143
|
+
"command": "python",
|
|
144
|
+
"args": ["/absolute/path/to/almega_mcp.py"],
|
|
145
|
+
"env": {
|
|
146
|
+
"ALMEGA_BACKEND": "stripe",
|
|
147
|
+
"STRIPE_SECRET_KEY": "sk_test_..."
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Restart Claude Desktop. Claude can now open wallets, attempt payments,
|
|
155
|
+
and ask you to approve sensitive ones โ all reflected live in Stripe.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Demo script (copy-paste into Claude)
|
|
160
|
+
|
|
161
|
+
> Open a wallet for an agent called `research-bot` with a $50 monthly
|
|
162
|
+
> limit, allowing `api` and `saas` categories, and requiring approval
|
|
163
|
+
> above $25. Then have the agent try the following three payments:
|
|
164
|
+
>
|
|
165
|
+
> 1. $12 to `openai.com` (category: `api`)
|
|
166
|
+
> 2. $30 to `vercel.com` (category: `saas`)
|
|
167
|
+
> 3. $800 to `luxury-store.io` (category: `retail`)
|
|
168
|
+
>
|
|
169
|
+
> Show me the resulting ledger.
|
|
170
|
+
|
|
171
|
+
You'll see exactly what the landing's "Exhibit A" shows: the first one
|
|
172
|
+
approved, the second held for your sign-off, the third blocked.
|
|
173
|
+
|
|
174
|
+
If you're on the Stripe backend, refresh
|
|
175
|
+
<https://dashboard.stripe.com/test/issuing/authorizations> while you run
|
|
176
|
+
the prompt โ they appear live.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## What's deliberately missing (for now)
|
|
181
|
+
|
|
182
|
+
- **Persistence** โ wallets live in memory. Restart wipes the local index.
|
|
183
|
+
On the Stripe backend the Cardholders + Cards stay in Stripe, but the
|
|
184
|
+
link from `agent_id` to them is forgotten.
|
|
185
|
+
- **Webhook flow** โ for this demo Almega decides synchronously and tells
|
|
186
|
+
Stripe the outcome. Production flips this: Stripe sends an authorization
|
|
187
|
+
webhook and Almega decides on the wire.
|
|
188
|
+
- **Multi-tenant** โ single global ledger.
|
|
189
|
+
- **Auth** โ anyone with the MCP connection can do anything.
|
|
190
|
+
|
|
191
|
+
All of those are by design for this demo. The point is to make the
|
|
192
|
+
human-in-the-loop UX and the per-agent budget model **obvious in five
|
|
193
|
+
minutes**, not to ship a bank.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Where this fits
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
โโโโโโโโโโโโโโโโโโโ MCP tools โโโโโโโโโโโโโโโโ
|
|
201
|
+
โ Your AI agent โ โโโโโโโโโโโโโโโโโโโโโโบ โ Almega โ
|
|
202
|
+
โ (Claude, GPT, โ โ (this file) โ
|
|
203
|
+
โ LangChainโฆ) โ โโโโโ decision โโโโโโโ โ โ
|
|
204
|
+
โโโโโโโโโโโโโโโโโโโ โโโโโโโโฌโโโโโโโโ
|
|
205
|
+
โ
|
|
206
|
+
ALMEGA_BACKEND=
|
|
207
|
+
โ
|
|
208
|
+
memory โโโโโโโโโโโโโฌโโโโโโโบโโโโโโโโโ stripe
|
|
209
|
+
(in-process) โ (test mode)
|
|
210
|
+
โ
|
|
211
|
+
โผ
|
|
212
|
+
โโโโโโโโโโโโโโโโ
|
|
213
|
+
โ Stripe โ
|
|
214
|
+
โ Issuing โ
|
|
215
|
+
โ test mode โ
|
|
216
|
+
โโโโโโโโโโโโโโโโ
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT โ see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.almega-ai/almega-mcp -->
|
|
2
|
+
|
|
3
|
+
# ๐งช Almega MCP โ the demonstrator
|
|
4
|
+
|
|
5
|
+
> A wallet & guardrail for AI agents, exposed as a Model Context Protocol
|
|
6
|
+
> (MCP) server. Drop it into Claude Desktop, the Claude Agent SDK, or any
|
|
7
|
+
> MCP-compatible client, and your agent has a wallet with hard limits, a
|
|
8
|
+
> human approval step, and a full ledger โ instantly.
|
|
9
|
+
>
|
|
10
|
+
> Ships with **two backends** out of the box:
|
|
11
|
+
>
|
|
12
|
+
> - `memory` (default): everything in-process. Zero setup.
|
|
13
|
+
> - `stripe`: real Stripe Issuing test-mode Cardholders + virtual Cards.
|
|
14
|
+
> No real money. You watch the dashboard light up live.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Tools the server exposes
|
|
19
|
+
|
|
20
|
+
| Tool | What it does |
|
|
21
|
+
|------|--------------|
|
|
22
|
+
| `open_wallet(agent_id, monthly_limit, allow, approve_above)` | Give an agent a wallet (and a real Stripe card if backend=stripe) |
|
|
23
|
+
| `pay(agent_id, merchant, amount, category)` | Agent tries to spend โ gets `APPROVED`, `BLOCKED`, or `AWAITING_YOU` |
|
|
24
|
+
| `approve_pending(transaction_id)` | Human says yes to a held transaction |
|
|
25
|
+
| `reject_pending(transaction_id, reason)` | Human says no |
|
|
26
|
+
| `get_wallet(agent_id)` | Current balance & rules |
|
|
27
|
+
| `list_transactions(agent_id?, status?, limit)` | View the ledger |
|
|
28
|
+
| `reset()` | Wipe the local index (Stripe entities are kept) |
|
|
29
|
+
|
|
30
|
+
Plus two resources:
|
|
31
|
+
|
|
32
|
+
- `almega://wallets` โ pretty-printed list of every wallet
|
|
33
|
+
- `almega://ledger` โ pretty-printed full ledger
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -r requirements.txt
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
(Installs `mcp[cli]` and `stripe`. Python 3.10+ recommended.)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Option A โ Memory backend (30-second demo)
|
|
48
|
+
|
|
49
|
+
No accounts, no env vars. Just run:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mcp dev almega_mcp.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That opens the MCP Inspector in your browser. Call `open_wallet`, `pay`,
|
|
56
|
+
`approve_pending` by hand and watch the ledger update.
|
|
57
|
+
|
|
58
|
+
Or run the scripted scenario:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python demo.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Option B โ Stripe Issuing test mode (5 minutes, still $0)
|
|
67
|
+
|
|
68
|
+
Now the wallet maps to a **real Stripe Cardholder + virtual Card** and every
|
|
69
|
+
`pay()` creates a real **test-mode authorization**. You can open the Stripe
|
|
70
|
+
dashboard and see Almega's ledger reflected on Stripe in real time.
|
|
71
|
+
|
|
72
|
+
### Setup
|
|
73
|
+
|
|
74
|
+
1. Create a free Stripe account: <https://dashboard.stripe.com/register>
|
|
75
|
+
2. Activate Issuing in test mode: <https://dashboard.stripe.com/test/issuing/overview>
|
|
76
|
+
(Stripe asks for some business info even in test โ fill it in. Nothing
|
|
77
|
+
leaves test mode until you flip "Activate live".)
|
|
78
|
+
3. Grab your **TEST** secret key: <https://dashboard.stripe.com/test/apikeys>
|
|
79
|
+
|
|
80
|
+
### Run
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export STRIPE_SECRET_KEY=sk_test_...
|
|
84
|
+
export ALMEGA_BACKEND=stripe
|
|
85
|
+
python stripe_demo.py
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Almega refuses to start if your key isn't `sk_test_...` โ there's no path
|
|
89
|
+
to accidentally hit live cards from this code.
|
|
90
|
+
|
|
91
|
+
### What you'll see in the dashboard
|
|
92
|
+
|
|
93
|
+
- **<https://dashboard.stripe.com/test/issuing/cards>**
|
|
94
|
+
one virtual card per agent, with the agent name on it.
|
|
95
|
+
- **<https://dashboard.stripe.com/test/issuing/authorizations>**
|
|
96
|
+
every `pay()` call as a real Stripe authorization, marked
|
|
97
|
+
*approved* or *declined* exactly the way Almega decided.
|
|
98
|
+
|
|
99
|
+
The wiring is intentionally direct: Almega decides locally, then mirrors
|
|
100
|
+
the decision onto Stripe. The next step (Phase 4) flips it: Stripe sends a
|
|
101
|
+
webhook to your server and Almega decides *during* the authorization. The
|
|
102
|
+
demo here is the synchronous half โ both halves expose the same MCP
|
|
103
|
+
surface to the agent.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Wire it into Claude Desktop
|
|
108
|
+
|
|
109
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` on
|
|
110
|
+
macOS (or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"almega": {
|
|
116
|
+
"command": "python",
|
|
117
|
+
"args": ["/absolute/path/to/almega_mcp.py"],
|
|
118
|
+
"env": {
|
|
119
|
+
"ALMEGA_BACKEND": "stripe",
|
|
120
|
+
"STRIPE_SECRET_KEY": "sk_test_..."
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Restart Claude Desktop. Claude can now open wallets, attempt payments,
|
|
128
|
+
and ask you to approve sensitive ones โ all reflected live in Stripe.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Demo script (copy-paste into Claude)
|
|
133
|
+
|
|
134
|
+
> Open a wallet for an agent called `research-bot` with a $50 monthly
|
|
135
|
+
> limit, allowing `api` and `saas` categories, and requiring approval
|
|
136
|
+
> above $25. Then have the agent try the following three payments:
|
|
137
|
+
>
|
|
138
|
+
> 1. $12 to `openai.com` (category: `api`)
|
|
139
|
+
> 2. $30 to `vercel.com` (category: `saas`)
|
|
140
|
+
> 3. $800 to `luxury-store.io` (category: `retail`)
|
|
141
|
+
>
|
|
142
|
+
> Show me the resulting ledger.
|
|
143
|
+
|
|
144
|
+
You'll see exactly what the landing's "Exhibit A" shows: the first one
|
|
145
|
+
approved, the second held for your sign-off, the third blocked.
|
|
146
|
+
|
|
147
|
+
If you're on the Stripe backend, refresh
|
|
148
|
+
<https://dashboard.stripe.com/test/issuing/authorizations> while you run
|
|
149
|
+
the prompt โ they appear live.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## What's deliberately missing (for now)
|
|
154
|
+
|
|
155
|
+
- **Persistence** โ wallets live in memory. Restart wipes the local index.
|
|
156
|
+
On the Stripe backend the Cardholders + Cards stay in Stripe, but the
|
|
157
|
+
link from `agent_id` to them is forgotten.
|
|
158
|
+
- **Webhook flow** โ for this demo Almega decides synchronously and tells
|
|
159
|
+
Stripe the outcome. Production flips this: Stripe sends an authorization
|
|
160
|
+
webhook and Almega decides on the wire.
|
|
161
|
+
- **Multi-tenant** โ single global ledger.
|
|
162
|
+
- **Auth** โ anyone with the MCP connection can do anything.
|
|
163
|
+
|
|
164
|
+
All of those are by design for this demo. The point is to make the
|
|
165
|
+
human-in-the-loop UX and the per-agent budget model **obvious in five
|
|
166
|
+
minutes**, not to ship a bank.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Where this fits
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
โโโโโโโโโโโโโโโโโโโ MCP tools โโโโโโโโโโโโโโโโ
|
|
174
|
+
โ Your AI agent โ โโโโโโโโโโโโโโโโโโโโโโบ โ Almega โ
|
|
175
|
+
โ (Claude, GPT, โ โ (this file) โ
|
|
176
|
+
โ LangChainโฆ) โ โโโโโ decision โโโโโโโ โ โ
|
|
177
|
+
โโโโโโโโโโโโโโโโโโโ โโโโโโโโฌโโโโโโโโ
|
|
178
|
+
โ
|
|
179
|
+
ALMEGA_BACKEND=
|
|
180
|
+
โ
|
|
181
|
+
memory โโโโโโโโโโโโโฌโโโโโโโบโโโโโโโโโ stripe
|
|
182
|
+
(in-process) โ (test mode)
|
|
183
|
+
โ
|
|
184
|
+
โผ
|
|
185
|
+
โโโโโโโโโโโโโโโโ
|
|
186
|
+
โ Stripe โ
|
|
187
|
+
โ Issuing โ
|
|
188
|
+
โ test mode โ
|
|
189
|
+
โโโโโโโโโโโโโโโโ
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT โ see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: almega-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A wallet & guardrail for AI agents: per-agent spending limits, allow-listed categories, 1-click human approval, and a full audit ledger, backed by Stripe Issuing.
|
|
5
|
+
Author: Almega
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://alemgaai.netlify.app
|
|
8
|
+
Project-URL: Repository, https://github.com/almega-ai/almega-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/almega-ai/almega-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,ai-agents,agent-infrastructure,payments,stripe,fintech,guardrails,wallet,human-in-the-loop
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
24
|
+
Requires-Dist: stripe>=11.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<!-- mcp-name: io.github.almega-ai/almega-mcp -->
|
|
29
|
+
|
|
30
|
+
# ๐งช Almega MCP โ the demonstrator
|
|
31
|
+
|
|
32
|
+
> A wallet & guardrail for AI agents, exposed as a Model Context Protocol
|
|
33
|
+
> (MCP) server. Drop it into Claude Desktop, the Claude Agent SDK, or any
|
|
34
|
+
> MCP-compatible client, and your agent has a wallet with hard limits, a
|
|
35
|
+
> human approval step, and a full ledger โ instantly.
|
|
36
|
+
>
|
|
37
|
+
> Ships with **two backends** out of the box:
|
|
38
|
+
>
|
|
39
|
+
> - `memory` (default): everything in-process. Zero setup.
|
|
40
|
+
> - `stripe`: real Stripe Issuing test-mode Cardholders + virtual Cards.
|
|
41
|
+
> No real money. You watch the dashboard light up live.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Tools the server exposes
|
|
46
|
+
|
|
47
|
+
| Tool | What it does |
|
|
48
|
+
|------|--------------|
|
|
49
|
+
| `open_wallet(agent_id, monthly_limit, allow, approve_above)` | Give an agent a wallet (and a real Stripe card if backend=stripe) |
|
|
50
|
+
| `pay(agent_id, merchant, amount, category)` | Agent tries to spend โ gets `APPROVED`, `BLOCKED`, or `AWAITING_YOU` |
|
|
51
|
+
| `approve_pending(transaction_id)` | Human says yes to a held transaction |
|
|
52
|
+
| `reject_pending(transaction_id, reason)` | Human says no |
|
|
53
|
+
| `get_wallet(agent_id)` | Current balance & rules |
|
|
54
|
+
| `list_transactions(agent_id?, status?, limit)` | View the ledger |
|
|
55
|
+
| `reset()` | Wipe the local index (Stripe entities are kept) |
|
|
56
|
+
|
|
57
|
+
Plus two resources:
|
|
58
|
+
|
|
59
|
+
- `almega://wallets` โ pretty-printed list of every wallet
|
|
60
|
+
- `almega://ledger` โ pretty-printed full ledger
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install -r requirements.txt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(Installs `mcp[cli]` and `stripe`. Python 3.10+ recommended.)
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Option A โ Memory backend (30-second demo)
|
|
75
|
+
|
|
76
|
+
No accounts, no env vars. Just run:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mcp dev almega_mcp.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That opens the MCP Inspector in your browser. Call `open_wallet`, `pay`,
|
|
83
|
+
`approve_pending` by hand and watch the ledger update.
|
|
84
|
+
|
|
85
|
+
Or run the scripted scenario:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python demo.py
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Option B โ Stripe Issuing test mode (5 minutes, still $0)
|
|
94
|
+
|
|
95
|
+
Now the wallet maps to a **real Stripe Cardholder + virtual Card** and every
|
|
96
|
+
`pay()` creates a real **test-mode authorization**. You can open the Stripe
|
|
97
|
+
dashboard and see Almega's ledger reflected on Stripe in real time.
|
|
98
|
+
|
|
99
|
+
### Setup
|
|
100
|
+
|
|
101
|
+
1. Create a free Stripe account: <https://dashboard.stripe.com/register>
|
|
102
|
+
2. Activate Issuing in test mode: <https://dashboard.stripe.com/test/issuing/overview>
|
|
103
|
+
(Stripe asks for some business info even in test โ fill it in. Nothing
|
|
104
|
+
leaves test mode until you flip "Activate live".)
|
|
105
|
+
3. Grab your **TEST** secret key: <https://dashboard.stripe.com/test/apikeys>
|
|
106
|
+
|
|
107
|
+
### Run
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export STRIPE_SECRET_KEY=sk_test_...
|
|
111
|
+
export ALMEGA_BACKEND=stripe
|
|
112
|
+
python stripe_demo.py
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Almega refuses to start if your key isn't `sk_test_...` โ there's no path
|
|
116
|
+
to accidentally hit live cards from this code.
|
|
117
|
+
|
|
118
|
+
### What you'll see in the dashboard
|
|
119
|
+
|
|
120
|
+
- **<https://dashboard.stripe.com/test/issuing/cards>**
|
|
121
|
+
one virtual card per agent, with the agent name on it.
|
|
122
|
+
- **<https://dashboard.stripe.com/test/issuing/authorizations>**
|
|
123
|
+
every `pay()` call as a real Stripe authorization, marked
|
|
124
|
+
*approved* or *declined* exactly the way Almega decided.
|
|
125
|
+
|
|
126
|
+
The wiring is intentionally direct: Almega decides locally, then mirrors
|
|
127
|
+
the decision onto Stripe. The next step (Phase 4) flips it: Stripe sends a
|
|
128
|
+
webhook to your server and Almega decides *during* the authorization. The
|
|
129
|
+
demo here is the synchronous half โ both halves expose the same MCP
|
|
130
|
+
surface to the agent.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Wire it into Claude Desktop
|
|
135
|
+
|
|
136
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` on
|
|
137
|
+
macOS (or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"mcpServers": {
|
|
142
|
+
"almega": {
|
|
143
|
+
"command": "python",
|
|
144
|
+
"args": ["/absolute/path/to/almega_mcp.py"],
|
|
145
|
+
"env": {
|
|
146
|
+
"ALMEGA_BACKEND": "stripe",
|
|
147
|
+
"STRIPE_SECRET_KEY": "sk_test_..."
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Restart Claude Desktop. Claude can now open wallets, attempt payments,
|
|
155
|
+
and ask you to approve sensitive ones โ all reflected live in Stripe.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Demo script (copy-paste into Claude)
|
|
160
|
+
|
|
161
|
+
> Open a wallet for an agent called `research-bot` with a $50 monthly
|
|
162
|
+
> limit, allowing `api` and `saas` categories, and requiring approval
|
|
163
|
+
> above $25. Then have the agent try the following three payments:
|
|
164
|
+
>
|
|
165
|
+
> 1. $12 to `openai.com` (category: `api`)
|
|
166
|
+
> 2. $30 to `vercel.com` (category: `saas`)
|
|
167
|
+
> 3. $800 to `luxury-store.io` (category: `retail`)
|
|
168
|
+
>
|
|
169
|
+
> Show me the resulting ledger.
|
|
170
|
+
|
|
171
|
+
You'll see exactly what the landing's "Exhibit A" shows: the first one
|
|
172
|
+
approved, the second held for your sign-off, the third blocked.
|
|
173
|
+
|
|
174
|
+
If you're on the Stripe backend, refresh
|
|
175
|
+
<https://dashboard.stripe.com/test/issuing/authorizations> while you run
|
|
176
|
+
the prompt โ they appear live.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## What's deliberately missing (for now)
|
|
181
|
+
|
|
182
|
+
- **Persistence** โ wallets live in memory. Restart wipes the local index.
|
|
183
|
+
On the Stripe backend the Cardholders + Cards stay in Stripe, but the
|
|
184
|
+
link from `agent_id` to them is forgotten.
|
|
185
|
+
- **Webhook flow** โ for this demo Almega decides synchronously and tells
|
|
186
|
+
Stripe the outcome. Production flips this: Stripe sends an authorization
|
|
187
|
+
webhook and Almega decides on the wire.
|
|
188
|
+
- **Multi-tenant** โ single global ledger.
|
|
189
|
+
- **Auth** โ anyone with the MCP connection can do anything.
|
|
190
|
+
|
|
191
|
+
All of those are by design for this demo. The point is to make the
|
|
192
|
+
human-in-the-loop UX and the per-agent budget model **obvious in five
|
|
193
|
+
minutes**, not to ship a bank.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Where this fits
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
โโโโโโโโโโโโโโโโโโโ MCP tools โโโโโโโโโโโโโโโโ
|
|
201
|
+
โ Your AI agent โ โโโโโโโโโโโโโโโโโโโโโโบ โ Almega โ
|
|
202
|
+
โ (Claude, GPT, โ โ (this file) โ
|
|
203
|
+
โ LangChainโฆ) โ โโโโโ decision โโโโโโโ โ โ
|
|
204
|
+
โโโโโโโโโโโโโโโโโโโ โโโโโโโโฌโโโโโโโโ
|
|
205
|
+
โ
|
|
206
|
+
ALMEGA_BACKEND=
|
|
207
|
+
โ
|
|
208
|
+
memory โโโโโโโโโโโโโฌโโโโโโโบโโโโโโโโโ stripe
|
|
209
|
+
(in-process) โ (test mode)
|
|
210
|
+
โ
|
|
211
|
+
โผ
|
|
212
|
+
โโโโโโโโโโโโโโโโ
|
|
213
|
+
โ Stripe โ
|
|
214
|
+
โ Issuing โ
|
|
215
|
+
โ test mode โ
|
|
216
|
+
โโโโโโโโโโโโโโโโ
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT โ see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
almega_mcp.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
almega_mcp.egg-info/PKG-INFO
|
|
6
|
+
almega_mcp.egg-info/SOURCES.txt
|
|
7
|
+
almega_mcp.egg-info/dependency_links.txt
|
|
8
|
+
almega_mcp.egg-info/entry_points.txt
|
|
9
|
+
almega_mcp.egg-info/requires.txt
|
|
10
|
+
almega_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
almega_mcp
|
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Almega MCP Server โ a wallet & guardrail for AI agents
|
|
3
|
+
|
|
4
|
+
Drop this in as an MCP server and any MCP-compatible agent (Claude Desktop,
|
|
5
|
+
the Claude Agent SDK, custom agents, etc.) can:
|
|
6
|
+
|
|
7
|
+
1. Open a wallet for itself with a budget & category rules
|
|
8
|
+
2. Try to pay merchants โ Almega enforces the rules
|
|
9
|
+
3. Get blocked, approved, or held for human review
|
|
10
|
+
|
|
11
|
+
Two storage backends ship in this file:
|
|
12
|
+
|
|
13
|
+
- ``memory`` (default): everything lives in-process. Great for a 30-second
|
|
14
|
+
demo, no external accounts needed.
|
|
15
|
+
|
|
16
|
+
- ``stripe``: every wallet maps to a real Stripe Issuing test-mode
|
|
17
|
+
Cardholder + virtual Card. Every pay() creates a real test-mode
|
|
18
|
+
authorization on Stripe. You see actual cards and ledgers in the Stripe
|
|
19
|
+
dashboard. No real money moves.
|
|
20
|
+
|
|
21
|
+
Pick the backend via the ``ALMEGA_BACKEND`` env var (``memory`` or ``stripe``).
|
|
22
|
+
|
|
23
|
+
Install:
|
|
24
|
+
pip install -r requirements.txt
|
|
25
|
+
|
|
26
|
+
Run with the MCP CLI:
|
|
27
|
+
mcp dev almega_mcp.py
|
|
28
|
+
|
|
29
|
+
Or wire into Claude Desktop's config (see README.md).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
from dataclasses import dataclass, field, asdict
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
from enum import Enum
|
|
38
|
+
from typing import Optional, Protocol
|
|
39
|
+
|
|
40
|
+
from mcp.server.fastmcp import FastMCP
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
44
|
+
# Domain model
|
|
45
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
46
|
+
|
|
47
|
+
class Status(str, Enum):
|
|
48
|
+
APPROVED = "APPROVED"
|
|
49
|
+
BLOCKED = "BLOCKED"
|
|
50
|
+
AWAITING_YOU = "AWAITING_YOU"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Wallet:
|
|
55
|
+
agent_id: str
|
|
56
|
+
monthly_limit: float # in dollars
|
|
57
|
+
allow: list[str] # categories the agent can spend in
|
|
58
|
+
approve_above: float # any single charge above this needs human ok
|
|
59
|
+
spent_this_month: float = 0.0
|
|
60
|
+
# Backend-specific identifiers, populated by the backend when relevant.
|
|
61
|
+
cardholder_id: Optional[str] = None
|
|
62
|
+
card_id: Optional[str] = None
|
|
63
|
+
last4: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Transaction:
|
|
68
|
+
id: str
|
|
69
|
+
agent_id: str
|
|
70
|
+
merchant: str
|
|
71
|
+
amount: float
|
|
72
|
+
category: str
|
|
73
|
+
status: Status
|
|
74
|
+
reason: str
|
|
75
|
+
created_at: str
|
|
76
|
+
# Backend-specific identifier (Stripe authorization id, etc.)
|
|
77
|
+
external_id: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _now() -> str:
|
|
81
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def decide(wallet: Wallet, amount: float, category: str) -> tuple[Status, str]:
|
|
85
|
+
"""The whole policy engine, on purpose tiny and readable."""
|
|
86
|
+
if category not in wallet.allow:
|
|
87
|
+
return Status.BLOCKED, f"category '{category}' is not in allow-list {wallet.allow}"
|
|
88
|
+
|
|
89
|
+
remaining = wallet.monthly_limit - wallet.spent_this_month
|
|
90
|
+
if amount > remaining:
|
|
91
|
+
return Status.BLOCKED, (
|
|
92
|
+
f"would exceed monthly limit (${amount:.2f} requested, "
|
|
93
|
+
f"${remaining:.2f} left of ${wallet.monthly_limit:.2f})"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if amount > wallet.approve_above:
|
|
97
|
+
return Status.AWAITING_YOU, (
|
|
98
|
+
f"single charge above approval threshold "
|
|
99
|
+
f"(${amount:.2f} > ${wallet.approve_above:.2f}) โ held for human review"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return Status.APPROVED, "within budget, within rules"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
106
|
+
# Backend protocol
|
|
107
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
108
|
+
|
|
109
|
+
class Backend(Protocol):
|
|
110
|
+
"""Anything that can store/retrieve Almega state and (optionally) mirror
|
|
111
|
+
decisions onto a real payments rail."""
|
|
112
|
+
|
|
113
|
+
name: str
|
|
114
|
+
|
|
115
|
+
def create_wallet(self, wallet: Wallet) -> None: ...
|
|
116
|
+
def get_wallet(self, agent_id: str) -> Optional[Wallet]: ...
|
|
117
|
+
def all_wallets(self) -> list[Wallet]: ...
|
|
118
|
+
def record_transaction(self, tx: Transaction) -> None: ...
|
|
119
|
+
def update_transaction(self, tx: Transaction) -> None: ...
|
|
120
|
+
def get_transaction(self, tx_id: str) -> Optional[Transaction]: ...
|
|
121
|
+
def list_transactions(self) -> list[Transaction]: ...
|
|
122
|
+
def reset(self) -> None: ...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
126
|
+
# Memory backend (default)
|
|
127
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
128
|
+
|
|
129
|
+
class MemoryBackend:
|
|
130
|
+
name = "memory"
|
|
131
|
+
|
|
132
|
+
def __init__(self) -> None:
|
|
133
|
+
self._wallets: dict[str, Wallet] = {}
|
|
134
|
+
self._ledger: list[Transaction] = []
|
|
135
|
+
self._next_tx_id: int = 1
|
|
136
|
+
|
|
137
|
+
# internal id minting โ only used by the MCP layer
|
|
138
|
+
def next_tx_id(self) -> str:
|
|
139
|
+
tx_id = f"tx_{self._next_tx_id:04d}"
|
|
140
|
+
self._next_tx_id += 1
|
|
141
|
+
return tx_id
|
|
142
|
+
|
|
143
|
+
def create_wallet(self, wallet: Wallet) -> None:
|
|
144
|
+
self._wallets[wallet.agent_id] = wallet
|
|
145
|
+
|
|
146
|
+
def get_wallet(self, agent_id: str) -> Optional[Wallet]:
|
|
147
|
+
return self._wallets.get(agent_id)
|
|
148
|
+
|
|
149
|
+
def all_wallets(self) -> list[Wallet]:
|
|
150
|
+
return list(self._wallets.values())
|
|
151
|
+
|
|
152
|
+
def record_transaction(self, tx: Transaction) -> None:
|
|
153
|
+
self._ledger.append(tx)
|
|
154
|
+
|
|
155
|
+
def update_transaction(self, tx: Transaction) -> None:
|
|
156
|
+
# Transactions are mutable references in MemoryBackend; nothing to do.
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def get_transaction(self, tx_id: str) -> Optional[Transaction]:
|
|
160
|
+
for t in self._ledger:
|
|
161
|
+
if t.id == tx_id:
|
|
162
|
+
return t
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def list_transactions(self) -> list[Transaction]:
|
|
166
|
+
return list(self._ledger)
|
|
167
|
+
|
|
168
|
+
def reset(self) -> None:
|
|
169
|
+
self._wallets.clear()
|
|
170
|
+
self._ledger.clear()
|
|
171
|
+
self._next_tx_id = 1
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
175
|
+
# Stripe Issuing backend (test mode only)
|
|
176
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
177
|
+
|
|
178
|
+
class StripeBackend:
|
|
179
|
+
"""
|
|
180
|
+
Real Stripe Issuing test-mode integration. Every wallet maps to a real
|
|
181
|
+
Stripe Cardholder + virtual Card. Every pay() creates a real test-mode
|
|
182
|
+
authorization. No money moves.
|
|
183
|
+
|
|
184
|
+
Setup:
|
|
185
|
+
1. https://dashboard.stripe.com/test/issuing โ enable Issuing in test mode
|
|
186
|
+
2. Export your test API key: export STRIPE_SECRET_KEY=sk_test_...
|
|
187
|
+
3. Set: export ALMEGA_BACKEND=stripe
|
|
188
|
+
|
|
189
|
+
On first wallet creation, Almega:
|
|
190
|
+
- creates a Cardholder ('Agent: <agent_id>') in test mode
|
|
191
|
+
- issues a virtual Card to that cardholder
|
|
192
|
+
- returns the last-4 so the agent knows its card
|
|
193
|
+
|
|
194
|
+
On pay():
|
|
195
|
+
- Almega's local policy decides APPROVED / BLOCKED / AWAITING_YOU
|
|
196
|
+
- a real test-mode authorization is created on Stripe with that outcome
|
|
197
|
+
- the Stripe dashboard shows the exact ledger Almega shows
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
name = "stripe"
|
|
201
|
+
|
|
202
|
+
def __init__(self) -> None:
|
|
203
|
+
api_key = os.environ.get("STRIPE_SECRET_KEY")
|
|
204
|
+
if not api_key:
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
"ALMEGA_BACKEND=stripe but STRIPE_SECRET_KEY is not set. "
|
|
207
|
+
"Export your test-mode key: sk_test_..."
|
|
208
|
+
)
|
|
209
|
+
if not api_key.startswith("sk_test_"):
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
"STRIPE_SECRET_KEY is not a TEST key. Almega refuses to run "
|
|
212
|
+
"against live Stripe. Use sk_test_..."
|
|
213
|
+
)
|
|
214
|
+
try:
|
|
215
|
+
import stripe # type: ignore
|
|
216
|
+
import requests # type: ignore
|
|
217
|
+
except ModuleNotFoundError as e:
|
|
218
|
+
raise RuntimeError(
|
|
219
|
+
"Missing dependencies. Run: pip install -r requirements.txt"
|
|
220
|
+
) from e
|
|
221
|
+
stripe.api_key = api_key
|
|
222
|
+
self.stripe = stripe
|
|
223
|
+
self._api_key = api_key
|
|
224
|
+
self._requests = requests
|
|
225
|
+
|
|
226
|
+
# We still keep an in-process index so MCP lookups don't hammer Stripe.
|
|
227
|
+
self._wallets: dict[str, Wallet] = {}
|
|
228
|
+
self._ledger: list[Transaction] = []
|
|
229
|
+
|
|
230
|
+
def _create_test_authorization(self, card_id: str, amount_cents: int, merchant: str) -> dict:
|
|
231
|
+
"""
|
|
232
|
+
Test-helper endpoint to simulate a merchant authorization in test mode.
|
|
233
|
+
Use raw HTTP so we don't depend on a specific stripe-python namespace
|
|
234
|
+
layout (it has shifted across SDK versions).
|
|
235
|
+
Docs: https://stripe.com/docs/api/issuing/authorizations/create_test_mode
|
|
236
|
+
"""
|
|
237
|
+
resp = self._requests.post(
|
|
238
|
+
"https://api.stripe.com/v1/test_helpers/issuing/authorizations",
|
|
239
|
+
auth=(self._api_key, ""),
|
|
240
|
+
data={
|
|
241
|
+
"card": card_id,
|
|
242
|
+
"amount": amount_cents,
|
|
243
|
+
"currency": "eur",
|
|
244
|
+
"merchant_data[name]": merchant,
|
|
245
|
+
"merchant_data[category]": "computer_software_stores",
|
|
246
|
+
"merchant_data[city]": "Internet",
|
|
247
|
+
"merchant_data[country]": "FR",
|
|
248
|
+
},
|
|
249
|
+
timeout=15,
|
|
250
|
+
)
|
|
251
|
+
resp.raise_for_status()
|
|
252
|
+
return resp.json()
|
|
253
|
+
|
|
254
|
+
# ---- wallets ----
|
|
255
|
+
|
|
256
|
+
def create_wallet(self, wallet: Wallet) -> None:
|
|
257
|
+
# Stripe Issuing's `name` field rejects numbers and special chars,
|
|
258
|
+
# so we turn the agent_id into a clean letters-only display name and
|
|
259
|
+
# stash the real id in metadata.
|
|
260
|
+
import re
|
|
261
|
+
import time
|
|
262
|
+
parts = re.findall(r"[A-Za-z]+", wallet.agent_id)
|
|
263
|
+
display = " ".join(p.capitalize() for p in parts) if parts else "Bot"
|
|
264
|
+
stripe_name = f"Almega {display}"
|
|
265
|
+
first_name = "Almega"
|
|
266
|
+
last_name = "".join(parts).capitalize() or "Bot"
|
|
267
|
+
|
|
268
|
+
# Stripe-friendly email: lowercase letters only, fall back to agent
|
|
269
|
+
email_local = re.sub(r"[^a-z0-9_-]", "", wallet.agent_id.lower()) or "agent"
|
|
270
|
+
|
|
271
|
+
# FR Issuing requires the cardholder to "accept" the issuing user terms,
|
|
272
|
+
# passed as an IP+timestamp on cardholder creation.
|
|
273
|
+
terms_acceptance = {
|
|
274
|
+
"date": int(time.time()),
|
|
275
|
+
"ip": "127.0.0.1",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
ch = self.stripe.issuing.Cardholder.create(
|
|
279
|
+
type="individual",
|
|
280
|
+
name=stripe_name,
|
|
281
|
+
email=f"{email_local}@almega.dev",
|
|
282
|
+
phone_number="+33612345678",
|
|
283
|
+
billing={"address": {
|
|
284
|
+
"line1": "1 Rue Almega",
|
|
285
|
+
"city": "Paris",
|
|
286
|
+
"postal_code": "75001",
|
|
287
|
+
"country": "FR",
|
|
288
|
+
}},
|
|
289
|
+
individual={
|
|
290
|
+
"first_name": first_name,
|
|
291
|
+
"last_name": last_name,
|
|
292
|
+
"card_issuing": {
|
|
293
|
+
"user_terms_acceptance": terms_acceptance,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
metadata={"almega_agent_id": wallet.agent_id},
|
|
297
|
+
)
|
|
298
|
+
card = self.stripe.issuing.Card.create(
|
|
299
|
+
cardholder=ch["id"],
|
|
300
|
+
currency="eur",
|
|
301
|
+
type="virtual",
|
|
302
|
+
status="active",
|
|
303
|
+
)
|
|
304
|
+
wallet.cardholder_id = ch["id"]
|
|
305
|
+
wallet.card_id = card["id"]
|
|
306
|
+
try:
|
|
307
|
+
wallet.last4 = card["last4"]
|
|
308
|
+
except (KeyError, AttributeError):
|
|
309
|
+
wallet.last4 = None
|
|
310
|
+
self._wallets[wallet.agent_id] = wallet
|
|
311
|
+
|
|
312
|
+
def get_wallet(self, agent_id: str) -> Optional[Wallet]:
|
|
313
|
+
return self._wallets.get(agent_id)
|
|
314
|
+
|
|
315
|
+
def all_wallets(self) -> list[Wallet]:
|
|
316
|
+
return list(self._wallets.values())
|
|
317
|
+
|
|
318
|
+
# ---- transactions ----
|
|
319
|
+
|
|
320
|
+
def record_transaction(self, tx: Transaction) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Almega is the gate before Stripe. Only transactions Almega APPROVED
|
|
323
|
+
actually reach Stripe โ they show up as real test-mode authorizations
|
|
324
|
+
on the card. BLOCKED and AWAITING_YOU transactions are held at the
|
|
325
|
+
gate and never touch the card.
|
|
326
|
+
|
|
327
|
+
In production this maps cleanly: Stripe sends a webhook on every
|
|
328
|
+
merchant authorization, Almega decides in-flight, and Stripe finalizes
|
|
329
|
+
from Almega's decision. Here in test mode we model the same idea
|
|
330
|
+
without a webhook listener: Almega decides first, then mirrors only
|
|
331
|
+
the green-lit transactions onto Stripe.
|
|
332
|
+
"""
|
|
333
|
+
wallet = self._wallets.get(tx.agent_id)
|
|
334
|
+
if wallet and wallet.card_id and tx.status is Status.APPROVED:
|
|
335
|
+
amount_cents = int(round(tx.amount * 100))
|
|
336
|
+
auth = self._create_test_authorization(wallet.card_id, amount_cents, tx.merchant)
|
|
337
|
+
tx.external_id = auth["id"]
|
|
338
|
+
# BLOCKED and AWAITING_YOU: no Stripe call. The card stays clean.
|
|
339
|
+
self._ledger.append(tx)
|
|
340
|
+
|
|
341
|
+
def update_transaction(self, tx: Transaction) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Called when a human approves/rejects a held transaction.
|
|
344
|
+
On approval, NOW we send the tx through to Stripe โ the gate opened.
|
|
345
|
+
On rejection, nothing reaches Stripe.
|
|
346
|
+
"""
|
|
347
|
+
if tx.status is not Status.APPROVED:
|
|
348
|
+
return # rejected โ stays at the gate
|
|
349
|
+
if tx.external_id:
|
|
350
|
+
return # already mirrored
|
|
351
|
+
wallet = self._wallets.get(tx.agent_id)
|
|
352
|
+
if not wallet or not wallet.card_id:
|
|
353
|
+
return
|
|
354
|
+
amount_cents = int(round(tx.amount * 100))
|
|
355
|
+
auth = self._create_test_authorization(wallet.card_id, amount_cents, tx.merchant)
|
|
356
|
+
tx.external_id = auth["id"]
|
|
357
|
+
|
|
358
|
+
def get_transaction(self, tx_id: str) -> Optional[Transaction]:
|
|
359
|
+
for t in self._ledger:
|
|
360
|
+
if t.id == tx_id:
|
|
361
|
+
return t
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
def list_transactions(self) -> list[Transaction]:
|
|
365
|
+
return list(self._ledger)
|
|
366
|
+
|
|
367
|
+
def reset(self) -> None:
|
|
368
|
+
# We don't delete Stripe entities โ just forget the local index.
|
|
369
|
+
self._wallets.clear()
|
|
370
|
+
self._ledger.clear()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
374
|
+
# Backend selection
|
|
375
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
376
|
+
|
|
377
|
+
def make_backend() -> Backend:
|
|
378
|
+
choice = os.environ.get("ALMEGA_BACKEND", "memory").lower()
|
|
379
|
+
if choice == "memory":
|
|
380
|
+
return MemoryBackend()
|
|
381
|
+
if choice == "stripe":
|
|
382
|
+
return StripeBackend()
|
|
383
|
+
raise RuntimeError(
|
|
384
|
+
f"Unknown ALMEGA_BACKEND={choice!r}. Use 'memory' or 'stripe'."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
backend: Backend = make_backend()
|
|
389
|
+
|
|
390
|
+
# tx id minting โ works for both backends
|
|
391
|
+
_next_tx_id = 1
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _mint_id() -> str:
|
|
395
|
+
global _next_tx_id
|
|
396
|
+
tx_id = f"tx_{_next_tx_id:04d}"
|
|
397
|
+
_next_tx_id += 1
|
|
398
|
+
return tx_id
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
402
|
+
# MCP server
|
|
403
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
404
|
+
|
|
405
|
+
mcp = FastMCP("Almega")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@mcp.tool()
|
|
409
|
+
def open_wallet(
|
|
410
|
+
agent_id: str,
|
|
411
|
+
monthly_limit: float,
|
|
412
|
+
allow: list[str],
|
|
413
|
+
approve_above: float = 25.0,
|
|
414
|
+
) -> dict:
|
|
415
|
+
"""
|
|
416
|
+
Open a wallet for an agent.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
agent_id: A stable id for the agent (e.g. "research-bot").
|
|
420
|
+
monthly_limit: Max total spend per calendar month, in dollars.
|
|
421
|
+
allow: List of allowed merchant categories (e.g. ["api", "saas"]).
|
|
422
|
+
approve_above: Any single charge above this requires a human approval.
|
|
423
|
+
|
|
424
|
+
Returns: the created wallet (including Stripe IDs if backend=stripe).
|
|
425
|
+
"""
|
|
426
|
+
if backend.get_wallet(agent_id) is not None:
|
|
427
|
+
return {"error": f"wallet for '{agent_id}' already exists"}
|
|
428
|
+
w = Wallet(
|
|
429
|
+
agent_id=agent_id,
|
|
430
|
+
monthly_limit=float(monthly_limit),
|
|
431
|
+
allow=list(allow),
|
|
432
|
+
approve_above=float(approve_above),
|
|
433
|
+
)
|
|
434
|
+
backend.create_wallet(w)
|
|
435
|
+
return {"ok": True, "backend": backend.name, "wallet": asdict(w)}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@mcp.tool()
|
|
439
|
+
def pay(
|
|
440
|
+
agent_id: str,
|
|
441
|
+
merchant: str,
|
|
442
|
+
amount: float,
|
|
443
|
+
category: str,
|
|
444
|
+
) -> dict:
|
|
445
|
+
"""
|
|
446
|
+
Have an agent try to pay a merchant. Almega applies the rules and either
|
|
447
|
+
approves the transaction, blocks it, or holds it for human approval.
|
|
448
|
+
|
|
449
|
+
On the Stripe backend, a real test-mode authorization is created on the
|
|
450
|
+
agent's virtual card so the outcome shows up in the Stripe dashboard.
|
|
451
|
+
|
|
452
|
+
Returns the resulting transaction record.
|
|
453
|
+
"""
|
|
454
|
+
wallet = backend.get_wallet(agent_id)
|
|
455
|
+
if wallet is None:
|
|
456
|
+
return {"error": f"no wallet for '{agent_id}'. Call open_wallet first."}
|
|
457
|
+
|
|
458
|
+
status, reason = decide(wallet, float(amount), category)
|
|
459
|
+
tx = Transaction(
|
|
460
|
+
id=_mint_id(),
|
|
461
|
+
agent_id=agent_id,
|
|
462
|
+
merchant=merchant,
|
|
463
|
+
amount=round(float(amount), 2),
|
|
464
|
+
category=category,
|
|
465
|
+
status=status,
|
|
466
|
+
reason=reason,
|
|
467
|
+
created_at=_now(),
|
|
468
|
+
)
|
|
469
|
+
if status is Status.APPROVED:
|
|
470
|
+
wallet.spent_this_month = round(wallet.spent_this_month + tx.amount, 2)
|
|
471
|
+
|
|
472
|
+
backend.record_transaction(tx)
|
|
473
|
+
return asdict(tx)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@mcp.tool()
|
|
477
|
+
def approve_pending(transaction_id: str) -> dict:
|
|
478
|
+
"""
|
|
479
|
+
Human approval for a transaction that was held (AWAITING_YOU).
|
|
480
|
+
Marks it APPROVED and applies the spend to the wallet.
|
|
481
|
+
"""
|
|
482
|
+
tx = backend.get_transaction(transaction_id)
|
|
483
|
+
if tx is None:
|
|
484
|
+
return {"error": f"no transaction with id {transaction_id}"}
|
|
485
|
+
if tx.status is not Status.AWAITING_YOU:
|
|
486
|
+
return {"error": f"transaction {transaction_id} is {tx.status}, not pending"}
|
|
487
|
+
wallet = backend.get_wallet(tx.agent_id)
|
|
488
|
+
if wallet is None:
|
|
489
|
+
return {"error": f"wallet for '{tx.agent_id}' has disappeared"}
|
|
490
|
+
tx.status = Status.APPROVED
|
|
491
|
+
tx.reason = "approved by human"
|
|
492
|
+
wallet.spent_this_month = round(wallet.spent_this_month + tx.amount, 2)
|
|
493
|
+
backend.update_transaction(tx)
|
|
494
|
+
return {"ok": True, "transaction": asdict(tx)}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@mcp.tool()
|
|
498
|
+
def reject_pending(transaction_id: str, reason: str = "rejected by human") -> dict:
|
|
499
|
+
"""Human rejection of a transaction held for approval."""
|
|
500
|
+
tx = backend.get_transaction(transaction_id)
|
|
501
|
+
if tx is None:
|
|
502
|
+
return {"error": f"no transaction with id {transaction_id}"}
|
|
503
|
+
if tx.status is not Status.AWAITING_YOU:
|
|
504
|
+
return {"error": f"transaction {transaction_id} is {tx.status}, not pending"}
|
|
505
|
+
tx.status = Status.BLOCKED
|
|
506
|
+
tx.reason = reason
|
|
507
|
+
backend.update_transaction(tx)
|
|
508
|
+
return {"ok": True, "transaction": asdict(tx)}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@mcp.tool()
|
|
512
|
+
def get_wallet(agent_id: str) -> dict:
|
|
513
|
+
"""Get an agent's wallet โ limits, spend so far, remaining budget."""
|
|
514
|
+
wallet = backend.get_wallet(agent_id)
|
|
515
|
+
if wallet is None:
|
|
516
|
+
return {"error": f"no wallet for '{agent_id}'"}
|
|
517
|
+
d = asdict(wallet)
|
|
518
|
+
d["remaining"] = round(wallet.monthly_limit - wallet.spent_this_month, 2)
|
|
519
|
+
d["backend"] = backend.name
|
|
520
|
+
return d
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@mcp.tool()
|
|
524
|
+
def list_transactions(
|
|
525
|
+
agent_id: Optional[str] = None,
|
|
526
|
+
status: Optional[str] = None,
|
|
527
|
+
limit: int = 50,
|
|
528
|
+
) -> list[dict]:
|
|
529
|
+
"""
|
|
530
|
+
List recent transactions, optionally filtered.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
agent_id: If provided, only that agent's transactions.
|
|
534
|
+
status: One of APPROVED / BLOCKED / AWAITING_YOU. Case-insensitive.
|
|
535
|
+
limit: Max number of transactions to return (most recent first).
|
|
536
|
+
"""
|
|
537
|
+
rows = list(reversed(backend.list_transactions()))
|
|
538
|
+
if agent_id is not None:
|
|
539
|
+
rows = [t for t in rows if t.agent_id == agent_id]
|
|
540
|
+
if status is not None:
|
|
541
|
+
s = status.upper()
|
|
542
|
+
rows = [t for t in rows if t.status.value == s]
|
|
543
|
+
return [asdict(t) for t in rows[:limit]]
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@mcp.tool()
|
|
547
|
+
def reset() -> dict:
|
|
548
|
+
"""Wipe all wallets and the ledger (local index only โ Stripe entities are kept)."""
|
|
549
|
+
global _next_tx_id
|
|
550
|
+
backend.reset()
|
|
551
|
+
_next_tx_id = 1
|
|
552
|
+
return {"ok": True, "backend": backend.name}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
556
|
+
# Resources (read-only views the agent / Claude can consult any time)
|
|
557
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
558
|
+
|
|
559
|
+
@mcp.resource("almega://ledger")
|
|
560
|
+
def ledger_resource() -> str:
|
|
561
|
+
"""A printable view of the full ledger."""
|
|
562
|
+
rows = backend.list_transactions()
|
|
563
|
+
if not rows:
|
|
564
|
+
return f"(empty ledger โ no transactions yet ยท backend={backend.name})"
|
|
565
|
+
lines = [f"Almega ยท Account Ledger ยท backend={backend.name}", "-" * 76]
|
|
566
|
+
for tx in rows:
|
|
567
|
+
ext = f" [{tx.external_id}]" if tx.external_id else ""
|
|
568
|
+
lines.append(
|
|
569
|
+
f"{tx.id} {tx.agent_id:<16} โ {tx.merchant:<22} "
|
|
570
|
+
f"${tx.amount:>8.2f} {tx.status.value:<14} {tx.reason}{ext}"
|
|
571
|
+
)
|
|
572
|
+
return "\n".join(lines)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@mcp.resource("almega://wallets")
|
|
576
|
+
def wallets_resource() -> str:
|
|
577
|
+
"""A printable view of all open wallets."""
|
|
578
|
+
wallets = backend.all_wallets()
|
|
579
|
+
if not wallets:
|
|
580
|
+
return f"(no wallets opened yet ยท backend={backend.name})"
|
|
581
|
+
lines = [f"Almega ยท Wallets ยท backend={backend.name}", "-" * 76]
|
|
582
|
+
for w in wallets:
|
|
583
|
+
remaining = w.monthly_limit - w.spent_this_month
|
|
584
|
+
card = f" card=โขโขโขโข {w.last4}" if w.last4 else ""
|
|
585
|
+
lines.append(
|
|
586
|
+
f"{w.agent_id:<20} limit=${w.monthly_limit:>8.2f} "
|
|
587
|
+
f"spent=${w.spent_this_month:>8.2f} left=${remaining:>8.2f} "
|
|
588
|
+
f"allow={w.allow} approve_above=${w.approve_above:.2f}{card}"
|
|
589
|
+
)
|
|
590
|
+
return "\n".join(lines)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
594
|
+
|
|
595
|
+
def main() -> None:
|
|
596
|
+
"""Console entry point โ runs the Almega MCP server over stdio."""
|
|
597
|
+
mcp.run()
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
if __name__ == "__main__":
|
|
601
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "almega-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A wallet & guardrail for AI agents: per-agent spending limits, allow-listed categories, 1-click human approval, and a full audit ledger, backed by Stripe Issuing."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Almega" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"agent-infrastructure",
|
|
19
|
+
"payments",
|
|
20
|
+
"stripe",
|
|
21
|
+
"fintech",
|
|
22
|
+
"guardrails",
|
|
23
|
+
"wallet",
|
|
24
|
+
"human-in-the-loop",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Office/Business :: Financial",
|
|
35
|
+
"Topic :: Software Development :: Libraries",
|
|
36
|
+
]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"mcp[cli]>=1.0.0",
|
|
39
|
+
"stripe>=11.0.0",
|
|
40
|
+
"requests>=2.31.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://alemgaai.netlify.app"
|
|
45
|
+
Repository = "https://github.com/almega-ai/almega-mcp"
|
|
46
|
+
Issues = "https://github.com/almega-ai/almega-mcp/issues"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
almega-mcp = "almega_mcp:main"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
py-modules = ["almega_mcp"]
|