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.
@@ -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,2 @@
1
+ [console_scripts]
2
+ almega-mcp = almega_mcp:main
@@ -0,0 +1,3 @@
1
+ mcp[cli]>=1.0.0
2
+ stripe>=11.0.0
3
+ requests>=2.31.0
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+