@getbags/cash 0.2.1 → 0.3.1

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,72 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type CheckResponse, type DiscoverResponse, type FetchResponse, type BalanceResponse, type PoliciesResponse, type TransactionsResponse, type RedeemResponse, type MppSessionSummary } from './api.js';
3
+ export declare const WALLET_MCP_TOOL_NAMES: readonly ["wallet_balance", "wallet_discover", "wallet_search", "wallet_check_endpoint", "wallet_fetch", "wallet_sessions_list", "wallet_sessions_close", "wallet_sessions_tick", "wallet_redeem", "wallet_transactions", "wallet_get_settings", "wallet_update_settings"];
4
+ export type WalletMcpApi = {
5
+ getResult: (token: string, input: {
6
+ id: string;
7
+ field?: string;
8
+ offset?: number;
9
+ limit?: number;
10
+ }) => Promise<Record<string, unknown>>;
11
+ getBalance: (token: string) => Promise<BalanceResponse>;
12
+ discover: (token: string, query?: string) => Promise<DiscoverResponse>;
13
+ search: (token: string, query: string) => Promise<DiscoverResponse>;
14
+ checkEndpoint: (token: string, input: {
15
+ url: string;
16
+ method?: string;
17
+ body?: unknown;
18
+ }) => Promise<CheckResponse>;
19
+ walletFetch: (token: string, input: {
20
+ url: string;
21
+ method?: string;
22
+ headers?: Record<string, string>;
23
+ body?: unknown;
24
+ max_price_cents?: number;
25
+ payment_protocol?: 'x402' | 'mpp' | 'auto';
26
+ result_mode?: 'inline' | 'summary';
27
+ }) => Promise<FetchResponse>;
28
+ listSessions: (token: string) => Promise<{
29
+ sessions: MppSessionSummary[];
30
+ }>;
31
+ closeSessions: (token: string, input: {
32
+ session_id?: string;
33
+ channel_id?: string;
34
+ all?: boolean;
35
+ }) => Promise<{
36
+ closed: Array<{
37
+ id: string;
38
+ channel_id: string;
39
+ status: string;
40
+ }>;
41
+ errors: unknown[];
42
+ }>;
43
+ tickSession: (token: string, input: {
44
+ cumulative_cents: number;
45
+ session_id?: string;
46
+ channel_id?: string;
47
+ }) => Promise<Record<string, unknown>>;
48
+ redeem: (opts: {
49
+ code?: string;
50
+ mode?: 'live' | 'test';
51
+ }) => Promise<RedeemResponse>;
52
+ getTransactions: (token: string, opts?: {
53
+ limit?: number;
54
+ cursor?: string;
55
+ }) => Promise<TransactionsResponse>;
56
+ getPolicies: (token: string) => Promise<PoliciesResponse>;
57
+ setPolicies: (token: string, patch: Record<string, unknown>) => Promise<PoliciesResponse>;
58
+ };
59
+ export type WalletMcpServerDeps = {
60
+ /** Override credential lookup (default: ~/.bags/credentials.json). */
61
+ getToken?: () => string | null;
62
+ /** Persist a redeemed token (default: saveCredentials → ~/.bags/credentials.json). */
63
+ saveCredentials?: (token: string) => void;
64
+ api?: Partial<WalletMcpApi>;
65
+ };
66
+ /**
67
+ * Build the BAGS Cash wallet MCP server (stdio-ready). Token is read from
68
+ * ~/.bags/credentials.json (or deps.getToken) per tool call — never from config.
69
+ */
70
+ export declare function createWalletMcpServer(deps?: WalletMcpServerDeps): McpServer;
71
+ /** Connect the wallet MCP server over stdio (for `bags mcp`). */
72
+ export declare function runStdioServer(): Promise<void>;
@@ -0,0 +1,293 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { CliApiError, apiRequest, checkEndpoint, closeSessions, discover, getBalance, getResult, getPolicies, getTransactions, listSessions, redeem, search, setPolicies, walletFetch, } from './api.js';
6
+ import { loadCredentials, saveCredentials as defaultSaveCredentials } from './config.js';
7
+ import { WALLET_MCP_INSTRUCTIONS } from './mcp-instructions.js';
8
+ export const WALLET_MCP_TOOL_NAMES = [
9
+ 'wallet_balance',
10
+ 'wallet_discover',
11
+ 'wallet_search',
12
+ 'wallet_check_endpoint',
13
+ 'wallet_fetch',
14
+ 'wallet_sessions_list',
15
+ 'wallet_sessions_close',
16
+ 'wallet_sessions_tick',
17
+ 'wallet_redeem',
18
+ 'wallet_transactions',
19
+ 'wallet_get_settings',
20
+ 'wallet_update_settings',
21
+ ];
22
+ const MISSING_TOKEN_MESSAGE = 'No wallet token found. Run: npx @getbags/cash redeem --test (sandbox) or npx @getbags/cash redeem <code> (live invite).';
23
+ async function defaultTickSession(token, input) {
24
+ return apiRequest('/api/wallet/sessions', {
25
+ method: 'POST',
26
+ token,
27
+ body: {
28
+ action: 'tick',
29
+ cumulative_cents: input.cumulative_cents,
30
+ session_id: input.session_id,
31
+ channel_id: input.channel_id,
32
+ },
33
+ });
34
+ }
35
+ function textContent(data, isError = false) {
36
+ return {
37
+ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }],
38
+ isError,
39
+ };
40
+ }
41
+ function missingTokenResult() {
42
+ return textContent({
43
+ status: 'error',
44
+ code: 'AUTH_MISSING_CREDENTIALS',
45
+ message: MISSING_TOKEN_MESSAGE,
46
+ }, true);
47
+ }
48
+ function resolveApi(partial) {
49
+ return {
50
+ getBalance,
51
+ getResult,
52
+ discover,
53
+ search,
54
+ checkEndpoint,
55
+ walletFetch,
56
+ listSessions,
57
+ closeSessions,
58
+ tickSession: defaultTickSession,
59
+ redeem,
60
+ getTransactions,
61
+ getPolicies,
62
+ setPolicies,
63
+ ...partial,
64
+ };
65
+ }
66
+ function resolveToken(deps) {
67
+ if (deps.getToken)
68
+ return deps.getToken();
69
+ return loadCredentials()?.token ?? null;
70
+ }
71
+ /**
72
+ * Build the BAGS Cash wallet MCP server (stdio-ready). Token is read from
73
+ * ~/.bags/credentials.json (or deps.getToken) per tool call — never from config.
74
+ */
75
+ export function createWalletMcpServer(deps = {}) {
76
+ const api = resolveApi(deps.api);
77
+ const persistCredentials = deps.saveCredentials ?? defaultSaveCredentials;
78
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
79
+ const server = new McpServer({ name: 'bags-cash', version }, { instructions: WALLET_MCP_INSTRUCTIONS });
80
+ async function withToken(run) {
81
+ const token = resolveToken(deps);
82
+ if (!token)
83
+ return missingTokenResult();
84
+ try {
85
+ return await run(token);
86
+ }
87
+ catch (err) {
88
+ if (err instanceof CliApiError) {
89
+ return textContent(err.message, true);
90
+ }
91
+ return textContent({
92
+ status: 'error',
93
+ message: err instanceof Error ? err.message : String(err),
94
+ }, true);
95
+ }
96
+ }
97
+ server.registerTool('wallet_balance', {
98
+ title: 'Wallet balance',
99
+ description: 'Show BAGS agent wallet balance, daily spent/cap, per-call cap, and status.',
100
+ inputSchema: {},
101
+ annotations: { readOnlyHint: true, openWorldHint: false },
102
+ }, async () => withToken(async (token) => textContent(await api.getBalance(token))));
103
+ server.registerTool('wallet_discover', {
104
+ title: 'Discover wallet endpoints',
105
+ description: 'List BAGS merchant x402 endpoints. Optional query also searches the open ecosystem index (BAGS results first).',
106
+ inputSchema: {
107
+ query: z.string().optional().describe('Optional search keywords'),
108
+ },
109
+ annotations: { readOnlyHint: true, openWorldHint: true },
110
+ }, async (args) => (async () => {
111
+ const token = resolveToken(deps) ?? '';
112
+ const q = typeof args.query === 'string' ? args.query.trim() : '';
113
+ return textContent(await api.discover(token, q || undefined));
114
+ })());
115
+ server.registerTool('wallet_search', {
116
+ title: 'Search payable endpoints',
117
+ description: 'Natural-language search for x402 endpoints. BAGS catalog first, best-effort ecosystem (Bazaar) second. Returns price, description, schemas when available.',
118
+ inputSchema: {
119
+ query: z.string().min(1).describe('Natural-language or keyword query'),
120
+ },
121
+ annotations: { readOnlyHint: true, openWorldHint: true },
122
+ }, async (args) => textContent(await api.search(resolveToken(deps) ?? '', String(args.query ?? '').trim())));
123
+ server.registerTool('wallet_check_endpoint', {
124
+ title: 'Check endpoint quote',
125
+ description: 'Probe an x402 URL for schema + exact price quote without paying (AgentCash check parity). Use before wallet_fetch.',
126
+ inputSchema: {
127
+ url: z.string().url().describe('Endpoint URL to probe'),
128
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional(),
129
+ body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
130
+ },
131
+ annotations: { readOnlyHint: true, openWorldHint: true },
132
+ }, async (args) => withToken(async (token) => textContent(await api.checkEndpoint(token, {
133
+ url: String(args.url),
134
+ method: typeof args.method === 'string' ? args.method : undefined,
135
+ body: args.body,
136
+ }))));
137
+ server.registerTool('wallet_result', {
138
+ title: 'Read retained wallet result',
139
+ description: 'Retrieve full output, fields or array pages from a previous wallet_fetch result for 24 hours. Does not execute or charge again. Provider asset URLs may expire earlier.',
140
+ inputSchema: {
141
+ id: z.string().uuid(),
142
+ field: z.string().max(200).optional(),
143
+ offset: z.number().int().nonnegative().optional(),
144
+ limit: z.number().int().min(1).max(100).optional(),
145
+ },
146
+ annotations: { readOnlyHint: true, openWorldHint: false },
147
+ }, async (args) => withToken(async (token) => textContent(await api.getResult(token, args))));
148
+ server.registerTool('wallet_fetch', {
149
+ title: 'Wallet fetch (pay + proxy)',
150
+ description: 'Pay for an x402 or one-shot MPP tempo:charge HTTP request with the hosted wallet. Check the quote and wallet mode first. Hosted MPP sessions are currently unavailable.',
151
+ inputSchema: {
152
+ url: z.string().url(),
153
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).optional(),
154
+ body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
155
+ result_mode: z.enum(['inline', 'summary']).optional(),
156
+ max_price_cents: z.number().int().positive().optional(),
157
+ payment_protocol: z
158
+ .enum(['x402', 'mpp', 'auto'])
159
+ .optional()
160
+ .describe('Force rail (default auto: x402 then MPP-only fallback)'),
161
+ },
162
+ annotations: { readOnlyHint: false, openWorldHint: true },
163
+ }, async (args) => withToken(async (token) => textContent(await api.walletFetch(token, {
164
+ url: String(args.url),
165
+ method: typeof args.method === 'string' ? args.method : undefined,
166
+ body: args.body,
167
+ result_mode: args.result_mode,
168
+ max_price_cents: typeof args.max_price_cents === 'number' ? args.max_price_cents : undefined,
169
+ payment_protocol: args.payment_protocol === 'x402' ||
170
+ args.payment_protocol === 'mpp' ||
171
+ args.payment_protocol === 'auto'
172
+ ? args.payment_protocol
173
+ : undefined,
174
+ }))));
175
+ server.registerTool('wallet_sessions_list', {
176
+ title: 'List MPP sessions',
177
+ description: 'List open MPP tempo:session channels for the wallet (mppx / AgentCash sessions list parity).',
178
+ inputSchema: {
179
+ status: z.enum(['active', 'open', 'closing', 'closed', 'failed']).optional(),
180
+ },
181
+ annotations: { readOnlyHint: true, openWorldHint: false },
182
+ }, async () => withToken(async (token) => textContent(await api.listSessions(token))));
183
+ server.registerTool('wallet_sessions_close', {
184
+ title: 'Close MPP session',
185
+ description: 'Request cleanup of existing MPP sessions. On-chain close is currently unsupported: the call reports failure and preserves the reservation and ownership. New sessions cannot be opened.',
186
+ inputSchema: {
187
+ session_id: z.string().uuid().optional(),
188
+ channel_id: z.string().min(1).optional(),
189
+ all: z.boolean().optional(),
190
+ },
191
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true },
192
+ }, async (args) => withToken(async (token) => textContent(await api.closeSessions(token, {
193
+ session_id: typeof args.session_id === 'string' ? args.session_id : undefined,
194
+ channel_id: typeof args.channel_id === 'string' ? args.channel_id : undefined,
195
+ all: typeof args.all === 'boolean' ? args.all : undefined,
196
+ }))));
197
+ server.registerTool('wallet_sessions_tick', {
198
+ title: 'Tick MPP session spend',
199
+ description: 'Record cumulative spend against an open MPP session (server rejects above max_deposit). Used for metered/streaming vouchers.',
200
+ inputSchema: {
201
+ cumulative_cents: z.number().int().nonnegative(),
202
+ session_id: z.string().uuid().optional(),
203
+ channel_id: z.string().min(1).optional(),
204
+ },
205
+ annotations: { readOnlyHint: false, openWorldHint: false },
206
+ }, async (args) => withToken(async (token) => textContent(await api.tickSession(token, {
207
+ cumulative_cents: Number(args.cumulative_cents),
208
+ session_id: typeof args.session_id === 'string' ? args.session_id : undefined,
209
+ channel_id: typeof args.channel_id === 'string' ? args.channel_id : undefined,
210
+ }))));
211
+ server.registerTool('wallet_redeem', {
212
+ title: 'Redeem invite code',
213
+ description: 'Redeem a BAGS wallet invite code and persist the token to ~/.bags/credentials.json (never echoed back).',
214
+ inputSchema: {
215
+ code: z.string().min(1).describe('Invite code'),
216
+ },
217
+ annotations: { readOnlyHint: false, openWorldHint: false },
218
+ }, async (args) => {
219
+ try {
220
+ const result = await api.redeem({ code: String(args.code) });
221
+ persistCredentials(result.token);
222
+ return textContent({
223
+ status: 'ok',
224
+ message: 'Wallet token saved to ~/.bags/credentials.json (mode 0o600). Token is not returned — call wallet_balance next.',
225
+ balance_cents: result.balance_cents,
226
+ });
227
+ }
228
+ catch (err) {
229
+ if (err instanceof CliApiError) {
230
+ return textContent(err.message, true);
231
+ }
232
+ return textContent({
233
+ status: 'error',
234
+ message: err instanceof Error ? err.message : String(err),
235
+ }, true);
236
+ }
237
+ });
238
+ server.registerTool('wallet_transactions', {
239
+ title: 'Wallet transactions',
240
+ description: 'List paginated wallet ledger / spend history.',
241
+ inputSchema: {
242
+ limit: z.number().int().positive().max(100).optional(),
243
+ cursor: z.string().optional(),
244
+ },
245
+ annotations: { readOnlyHint: true, openWorldHint: false },
246
+ }, async (args) => withToken(async (token) => textContent(await api.getTransactions(token, {
247
+ limit: typeof args.limit === 'number' ? args.limit : undefined,
248
+ cursor: typeof args.cursor === 'string' ? args.cursor : undefined,
249
+ }))));
250
+ server.registerTool('wallet_get_settings', {
251
+ title: 'Get wallet settings / policies',
252
+ description: 'Read spend policies and wallet settings (caps, status).',
253
+ inputSchema: {},
254
+ annotations: { readOnlyHint: true, openWorldHint: false },
255
+ }, async () => withToken(async (token) => {
256
+ try {
257
+ return textContent(await api.getPolicies(token));
258
+ }
259
+ catch (err) {
260
+ if (err instanceof CliApiError && err.code === 'NOT_FOUND') {
261
+ const bal = await api.getBalance(token);
262
+ return textContent({
263
+ status: 'success',
264
+ data: { ...bal, note: 'policies API not live; returning balance caps' },
265
+ });
266
+ }
267
+ throw err;
268
+ }
269
+ }));
270
+ server.registerTool('wallet_update_settings', {
271
+ title: 'Update wallet settings / policies',
272
+ description: 'Patch spend policies (e.g. per_call_cap_cents, daily_cap_cents).',
273
+ inputSchema: {
274
+ per_call_cap_cents: z.number().int().positive().optional(),
275
+ daily_cap_cents: z.number().int().positive().optional(),
276
+ },
277
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
278
+ }, async (args) => withToken(async (token) => {
279
+ const patch = {};
280
+ if (typeof args.per_call_cap_cents === 'number')
281
+ patch.per_call_cap_cents = args.per_call_cap_cents;
282
+ if (typeof args.daily_cap_cents === 'number')
283
+ patch.daily_cap_cents = args.daily_cap_cents;
284
+ return textContent(await api.setPolicies(token, patch));
285
+ }));
286
+ return server;
287
+ }
288
+ /** Connect the wallet MCP server over stdio (for `bags mcp`). */
289
+ export async function runStdioServer() {
290
+ const server = createWalletMcpServer();
291
+ const transport = new StdioServerTransport();
292
+ await server.connect(transport);
293
+ }
package/dist/parse.d.ts CHANGED
@@ -1,11 +1,22 @@
1
+ import type { ClientId, InstallScope } from './detect.js';
1
2
  export type PaymentProtocol = 'x402' | 'mpp' | 'auto';
2
3
  export type ParsedArgs = {
3
4
  command: 'help';
5
+ } | {
6
+ command: 'usage';
7
+ message: string;
4
8
  } | {
5
9
  command: 'redeem';
6
- code: string;
10
+ code?: string;
11
+ test: boolean;
7
12
  } | {
8
13
  command: 'balance';
14
+ } | {
15
+ command: 'result';
16
+ id: string;
17
+ field?: string;
18
+ offset?: number;
19
+ limit?: number;
9
20
  } | {
10
21
  command: 'discover';
11
22
  format: 'json' | 'pretty';
@@ -27,6 +38,16 @@ export type ParsedArgs = {
27
38
  format: 'json' | 'pretty';
28
39
  } | {
29
40
  command: 'init';
41
+ dryRun: boolean;
42
+ clients: ClientId[];
43
+ scope: InstallScope;
44
+ } | {
45
+ command: 'mcp';
46
+ } | {
47
+ command: 'doctor';
48
+ clients: ClientId[];
49
+ /** Omitted means check both project and user scopes. */
50
+ scope?: InstallScope;
30
51
  } | {
31
52
  command: 'fund';
32
53
  open: boolean;