marqeta-diva-mcp 0.2.0__py3-none-any.whl

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,940 @@
1
+ """Marqeta DiVA API MCP Server."""
2
+
3
+ import asyncio
4
+ import os
5
+ import sys
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from dotenv import load_dotenv
9
+ from mcp.server import Server
10
+ from mcp.types import Tool, TextContent
11
+ import mcp.server.stdio
12
+
13
+ from .client import DiVAClient, DiVAAPIError
14
+
15
+ # Load environment variables
16
+ load_dotenv()
17
+
18
+ # Version identifier to verify server restarts
19
+ SERVER_VERSION = "0.2.0"
20
+
21
+ # Check if local storage features are enabled
22
+ def is_local_storage_enabled() -> bool:
23
+ """Check if local storage and RAG features are enabled."""
24
+ enabled = os.getenv("ENABLE_LOCAL_STORAGE", "false").lower() in ("true", "1", "yes")
25
+ return enabled
26
+
27
+ # Conditionally import RAG tools
28
+ RAG_AVAILABLE = False
29
+ rag_tools = None
30
+
31
+ if is_local_storage_enabled():
32
+ try:
33
+ from . import rag_tools
34
+ RAG_AVAILABLE = True
35
+ print("[MCP Server] Local storage and RAG features ENABLED", file=sys.stderr)
36
+ except ImportError as e:
37
+ print(f"[MCP Server] WARNING: ENABLE_LOCAL_STORAGE=true but RAG dependencies not installed.", file=sys.stderr)
38
+ print(f"[MCP Server] Install with: pip install marqeta-diva-mcp[rag]", file=sys.stderr)
39
+ print(f"[MCP Server] Error: {e}", file=sys.stderr)
40
+ RAG_AVAILABLE = False
41
+ else:
42
+ print("[MCP Server] Local storage and RAG features DISABLED (set ENABLE_LOCAL_STORAGE=true to enable)", file=sys.stderr)
43
+
44
+ # Initialize MCP server
45
+ app = Server("marqeta-diva-mcp")
46
+
47
+ # Log version on startup
48
+ print(f"[MCP Server] Starting version {SERVER_VERSION}", file=sys.stderr)
49
+
50
+ # Global client instance
51
+ _client: Optional[DiVAClient] = None
52
+
53
+
54
+ def get_client() -> DiVAClient:
55
+ """Get or create the DiVA API client."""
56
+ global _client
57
+ if _client is None:
58
+ app_token = os.getenv("MARQETA_APP_TOKEN")
59
+ access_token = os.getenv("MARQETA_ACCESS_TOKEN")
60
+ program = os.getenv("MARQETA_PROGRAM")
61
+
62
+ if not app_token or not access_token or not program:
63
+ raise ValueError(
64
+ "Missing required environment variables: "
65
+ "MARQETA_APP_TOKEN, MARQETA_ACCESS_TOKEN, MARQETA_PROGRAM"
66
+ )
67
+
68
+ _client = DiVAClient(app_token, access_token, program)
69
+ return _client
70
+
71
+
72
+ def format_error(error: Exception) -> str:
73
+ """Format an error message for display."""
74
+ if isinstance(error, DiVAAPIError):
75
+ msg = f"Error {error.status_code}: {error.message}"
76
+ if error.response:
77
+ msg += f"\nDetails: {error.response}"
78
+ return msg
79
+ return str(error)
80
+
81
+
82
+ def format_response(data: Dict[str, Any]) -> str:
83
+ """Format API response for display."""
84
+ import json
85
+ return json.dumps(data, indent=2)
86
+
87
+
88
+ # Tool definitions
89
+ # Base tools available to all users
90
+ BASE_TOOLS = [
91
+ # Transaction Tools
92
+ Tool(
93
+ name="get_authorizations",
94
+ description=(
95
+ "Get authorization transaction data. Includes authorization amounts, counts, "
96
+ "acting users/cards, and merchant information. Supports detail, day, week, and month aggregation. "
97
+ "Note: DiVA API limits results to 10,000 records per query. Use narrower date ranges or more specific filters for larger datasets."
98
+ ),
99
+ inputSchema={
100
+ "type": "object",
101
+ "properties": {
102
+ "aggregation": {
103
+ "type": "string",
104
+ "enum": ["detail", "day", "week", "month"],
105
+ "default": "detail",
106
+ "description": "Aggregation level for the data",
107
+ },
108
+ "fields": {
109
+ "type": "array",
110
+ "items": {"type": "string"},
111
+ "description": "Specific fields to return (comma-separated)",
112
+ },
113
+ "filters": {
114
+ "type": "object",
115
+ "description": (
116
+ "Filters on data fields. For date filtering, use the actual date field name with operators. "
117
+ "Example: {'transaction_timestamp': '>=2023-10-20'} or {'transaction_timestamp': '2023-10-01..2023-10-31'}. "
118
+ "Do NOT include query parameters like 'count' or 'sort_by' in filters."
119
+ ),
120
+ },
121
+ "sort_by": {
122
+ "type": "string",
123
+ "description": "Field to sort by (prefix with - for descending)",
124
+ },
125
+ "count": {
126
+ "type": "integer",
127
+ "description": "Maximum number of records to return (up to 10,000, default 10,000)",
128
+ },
129
+ "program": {
130
+ "type": "string",
131
+ "description": "Override default program name",
132
+ },
133
+ },
134
+ },
135
+ ),
136
+ Tool(
137
+ name="get_settlements",
138
+ description=(
139
+ "Get settlement transaction data. Includes transaction status, post dates, "
140
+ "purchase amounts, and network information. Supports detail, day, week, and month aggregation. "
141
+ "Note: DiVA API limits results to 10,000 records per query. Use narrower date ranges or more specific filters for larger datasets."
142
+ ),
143
+ inputSchema={
144
+ "type": "object",
145
+ "properties": {
146
+ "aggregation": {
147
+ "type": "string",
148
+ "enum": ["detail", "day", "week", "month"],
149
+ "default": "detail",
150
+ "description": "Aggregation level for the data",
151
+ },
152
+ "fields": {
153
+ "type": "array",
154
+ "items": {"type": "string"},
155
+ "description": "Specific fields to return",
156
+ },
157
+ "filters": {
158
+ "type": "object",
159
+ "description": (
160
+ "Filters on data fields. For date filtering, use actual date field names with operators. "
161
+ "Example: {'transaction_timestamp': '>=2023-10-20'}. Do NOT include query parameters like 'count' here."
162
+ ),
163
+ },
164
+ "sort_by": {"type": "string", "description": "Field to sort by"},
165
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
166
+ "program": {"type": "string", "description": "Override default program"},
167
+ },
168
+ },
169
+ ),
170
+ Tool(
171
+ name="get_clearings",
172
+ description=(
173
+ "Get clearing/reconciliation data. Provides accounting-level line items for "
174
+ "the transaction lifecycle. Ideal for reconciliation. Supports detail, day, week, and month aggregation. "
175
+ "Note: DiVA API limits results to 10,000 records per query. Use narrower date ranges or more specific filters for larger datasets."
176
+ ),
177
+ inputSchema={
178
+ "type": "object",
179
+ "properties": {
180
+ "aggregation": {
181
+ "type": "string",
182
+ "enum": ["detail", "day", "week", "month"],
183
+ "default": "detail",
184
+ "description": "Aggregation level for the data",
185
+ },
186
+ "fields": {
187
+ "type": "array",
188
+ "items": {"type": "string"},
189
+ "description": "Specific fields to return",
190
+ },
191
+ "filters": {
192
+ "type": "object",
193
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
194
+ },
195
+ "sort_by": {"type": "string", "description": "Field to sort by"},
196
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
197
+ "program": {"type": "string", "description": "Override default program"},
198
+ },
199
+ },
200
+ ),
201
+ Tool(
202
+ name="get_declines",
203
+ description=(
204
+ "Get declined transaction data. Includes transaction tokens, decline reasons, "
205
+ "merchant information, and amounts. Supports detail, day, week, and month aggregation. "
206
+ "Note: DiVA API limits results to 10,000 records per query. Use narrower date ranges or more specific filters for larger datasets."
207
+ ),
208
+ inputSchema={
209
+ "type": "object",
210
+ "properties": {
211
+ "aggregation": {
212
+ "type": "string",
213
+ "enum": ["detail", "day", "week", "month"],
214
+ "default": "detail",
215
+ "description": "Aggregation level for the data",
216
+ },
217
+ "fields": {
218
+ "type": "array",
219
+ "items": {"type": "string"},
220
+ "description": "Specific fields to return",
221
+ },
222
+ "filters": {
223
+ "type": "object",
224
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
225
+ },
226
+ "sort_by": {"type": "string", "description": "Field to sort by"},
227
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
228
+ "program": {"type": "string", "description": "Override default program"},
229
+ },
230
+ },
231
+ ),
232
+ Tool(
233
+ name="get_loads",
234
+ description=(
235
+ "Get load transaction data. Includes load amounts and transaction details. "
236
+ "Note: DiVA API limits results to 10,000 records per query. Use narrower date ranges or more specific filters for larger datasets."
237
+ ),
238
+ inputSchema={
239
+ "type": "object",
240
+ "properties": {
241
+ "aggregation": {
242
+ "type": "string",
243
+ "enum": ["detail", "day", "week", "month"],
244
+ "default": "detail",
245
+ "description": "Aggregation level for the data",
246
+ },
247
+ "fields": {
248
+ "type": "array",
249
+ "items": {"type": "string"},
250
+ "description": "Specific fields to return",
251
+ },
252
+ "filters": {
253
+ "type": "object",
254
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
255
+ },
256
+ "sort_by": {"type": "string", "description": "Field to sort by"},
257
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
258
+ "program": {"type": "string", "description": "Override default program"},
259
+ },
260
+ },
261
+ ),
262
+ # Financial Tools
263
+ Tool(
264
+ name="get_program_balances",
265
+ description=(
266
+ "Get program-level balance data. Includes beginning/ending bank balances, "
267
+ "amounts to send/receive. Day-level aggregation only."
268
+ ),
269
+ inputSchema={
270
+ "type": "object",
271
+ "properties": {
272
+ "fields": {
273
+ "type": "array",
274
+ "items": {"type": "string"},
275
+ "description": "Specific fields to return",
276
+ },
277
+ "filters": {
278
+ "type": "object",
279
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
280
+ },
281
+ "sort_by": {"type": "string", "description": "Field to sort by"},
282
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
283
+ "program": {"type": "string", "description": "Override default program"},
284
+ },
285
+ },
286
+ ),
287
+ Tool(
288
+ name="get_program_balances_settlement",
289
+ description=(
290
+ "Get settlement-based program balance data. Includes settlement balance information "
291
+ "and fund transfers. Day-level aggregation only."
292
+ ),
293
+ inputSchema={
294
+ "type": "object",
295
+ "properties": {
296
+ "fields": {
297
+ "type": "array",
298
+ "items": {"type": "string"},
299
+ "description": "Specific fields to return",
300
+ },
301
+ "filters": {
302
+ "type": "object",
303
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
304
+ },
305
+ "sort_by": {"type": "string", "description": "Field to sort by"},
306
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
307
+ "program": {"type": "string", "description": "Override default program"},
308
+ },
309
+ },
310
+ ),
311
+ Tool(
312
+ name="get_activity_balances",
313
+ description=(
314
+ "Get cardholder-level balance data. Includes individual cardholder balances, "
315
+ "expandable by network. Day-level aggregation only."
316
+ ),
317
+ inputSchema={
318
+ "type": "object",
319
+ "properties": {
320
+ "fields": {
321
+ "type": "array",
322
+ "items": {"type": "string"},
323
+ "description": "Specific fields to return",
324
+ },
325
+ "filters": {
326
+ "type": "object",
327
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
328
+ },
329
+ "sort_by": {"type": "string", "description": "Field to sort by"},
330
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
331
+ "expand": {
332
+ "type": "string",
333
+ "description": "Field to expand for more detail (e.g., network data)",
334
+ },
335
+ "program": {"type": "string", "description": "Override default program"},
336
+ },
337
+ },
338
+ ),
339
+ # Card & User Tools
340
+ Tool(
341
+ name="get_cards",
342
+ description=(
343
+ "Get card detail data. Includes user tokens, card state, active status, and UAI. "
344
+ "Detail-level only. Supports filtering by user, state, etc. "
345
+ "Note: DiVA API limits results to 10,000 records per query. Use narrower date ranges or more specific filters for larger datasets."
346
+ ),
347
+ inputSchema={
348
+ "type": "object",
349
+ "properties": {
350
+ "fields": {
351
+ "type": "array",
352
+ "items": {"type": "string"},
353
+ "description": "Specific fields to return",
354
+ },
355
+ "filters": {
356
+ "type": "object",
357
+ "description": "Filters (e.g., {'state': 'ACTIVE', 'user_token': 'abc123'})",
358
+ },
359
+ "sort_by": {"type": "string", "description": "Field to sort by"},
360
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
361
+ "program": {"type": "string", "description": "Override default program"},
362
+ },
363
+ },
364
+ ),
365
+ Tool(
366
+ name="get_users",
367
+ description=(
368
+ "Get user detail data. Includes user tokens, UAI, and number of physical/virtual cards. "
369
+ "Detail-level only. Note: DiVA API limits results to 10,000 records per query. Use more specific filters for larger datasets."
370
+ ),
371
+ inputSchema={
372
+ "type": "object",
373
+ "properties": {
374
+ "fields": {
375
+ "type": "array",
376
+ "items": {"type": "string"},
377
+ "description": "Specific fields to return",
378
+ },
379
+ "filters": {
380
+ "type": "object",
381
+ "description": "Filters (e.g., {'user_token': 'abc123'})",
382
+ },
383
+ "sort_by": {"type": "string", "description": "Field to sort by"},
384
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
385
+ "program": {"type": "string", "description": "Override default program"},
386
+ },
387
+ },
388
+ ),
389
+ # Chargeback Tools
390
+ Tool(
391
+ name="get_chargebacks_status",
392
+ description=(
393
+ "Get chargeback status data. Includes chargeback state, tokens, and "
394
+ "provisional credit status."
395
+ ),
396
+ inputSchema={
397
+ "type": "object",
398
+ "properties": {
399
+ "fields": {
400
+ "type": "array",
401
+ "items": {"type": "string"},
402
+ "description": "Specific fields to return",
403
+ },
404
+ "filters": {
405
+ "type": "object",
406
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
407
+ },
408
+ "sort_by": {"type": "string", "description": "Field to sort by"},
409
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
410
+ "program": {"type": "string", "description": "Override default program"},
411
+ },
412
+ },
413
+ ),
414
+ Tool(
415
+ name="get_chargebacks_detail",
416
+ description=(
417
+ "Get detailed chargeback information. Includes transaction dates/types and "
418
+ "comprehensive chargeback details."
419
+ ),
420
+ inputSchema={
421
+ "type": "object",
422
+ "properties": {
423
+ "fields": {
424
+ "type": "array",
425
+ "items": {"type": "string"},
426
+ "description": "Specific fields to return",
427
+ },
428
+ "filters": {
429
+ "type": "object",
430
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'post_date': '>=2023-10-20'})"
431
+ },
432
+ "sort_by": {"type": "string", "description": "Field to sort by"},
433
+ "count": {"type": "integer", "description": "Maximum records to return (up to 10,000, default 10,000)"},
434
+ "program": {"type": "string", "description": "Override default program"},
435
+ },
436
+ },
437
+ ),
438
+ # Metadata Tools
439
+ Tool(
440
+ name="list_available_views",
441
+ description=(
442
+ "Get a list of all available DiVA API view endpoints with metadata. "
443
+ "Useful for discovering available data sources."
444
+ ),
445
+ inputSchema={"type": "object", "properties": {}},
446
+ ),
447
+ Tool(
448
+ name="get_view_schema",
449
+ description=(
450
+ "Get the schema definition for any view endpoint. Returns field names, "
451
+ "data types, and descriptions."
452
+ ),
453
+ inputSchema={
454
+ "type": "object",
455
+ "required": ["view_name"],
456
+ "properties": {
457
+ "view_name": {
458
+ "type": "string",
459
+ "description": "Name of the view (e.g., 'authorizations', 'settlements', 'cards')",
460
+ },
461
+ "aggregation": {
462
+ "type": "string",
463
+ "enum": ["detail", "day", "week", "month"],
464
+ "default": "detail",
465
+ "description": "Aggregation level (if applicable)",
466
+ },
467
+ },
468
+ },
469
+ ),
470
+ # Export Tools
471
+ Tool(
472
+ name="export_view_to_file",
473
+ description=(
474
+ "Export datasets to a file (JSON or CSV). Note: DiVA API limits results to 10,000 records per query. "
475
+ "To get more data, use narrower date ranges or more specific filters and call multiple times. "
476
+ "Supports all view types: authorizations, settlements, clearings, declines, loads, cards, users, etc."
477
+ ),
478
+ inputSchema={
479
+ "type": "object",
480
+ "required": ["view_name", "output_path"],
481
+ "properties": {
482
+ "view_name": {
483
+ "type": "string",
484
+ "description": "Name of the view (e.g., 'authorizations', 'settlements', 'cards')",
485
+ },
486
+ "aggregation": {
487
+ "type": "string",
488
+ "enum": ["detail", "day", "week", "month"],
489
+ "default": "detail",
490
+ "description": "Aggregation level",
491
+ },
492
+ "output_path": {
493
+ "type": "string",
494
+ "description": "File path where data will be written (e.g., './exports/authorizations.json')",
495
+ },
496
+ "format": {
497
+ "type": "string",
498
+ "enum": ["json", "csv"],
499
+ "default": "json",
500
+ "description": "Output file format",
501
+ },
502
+ "max_records": {
503
+ "type": "integer",
504
+ "description": "Maximum total records to export (omit to export all matching records)",
505
+ },
506
+ "fields": {
507
+ "type": "array",
508
+ "items": {"type": "string"},
509
+ "description": "Specific fields to include in export",
510
+ },
511
+ "filters": {
512
+ "type": "object",
513
+ "description": "Filters on data fields. For date filtering, use actual date field names with operators (e.g., {'transaction_timestamp': '>=2023-10-20'})",
514
+ },
515
+ "sort_by": {"type": "string", "description": "Field to sort by"},
516
+ "program": {"type": "string", "description": "Override default program"},
517
+ },
518
+ },
519
+ ),
520
+ Tool(
521
+ name="get_server_version",
522
+ description=(
523
+ "Get the MCP server version to verify it has been restarted with latest code changes."
524
+ ),
525
+ inputSchema={"type": "object", "properties": {}},
526
+ ),
527
+ ]
528
+
529
+ # RAG tools - only available when ENABLE_LOCAL_STORAGE=true
530
+ RAG_TOOLS = [
531
+ Tool(
532
+ name="index_transactions",
533
+ description=(
534
+ "Sync transactions from DiVA to local storage (SQLite + ChromaDB). "
535
+ "Stores FULL transaction data locally and generates embeddings for semantic search. "
536
+ "This eliminates token limits - query locally without MCP constraints! "
537
+ "Must be called before using semantic search or local queries."
538
+ ),
539
+ inputSchema={
540
+ "type": "object",
541
+ "properties": {
542
+ "view_name": {
543
+ "type": "string",
544
+ "default": "authorizations",
545
+ "description": "DiVA view to index (default: authorizations)",
546
+ },
547
+ "aggregation": {
548
+ "type": "string",
549
+ "enum": ["detail", "day", "week", "month"],
550
+ "default": "detail",
551
+ "description": "Aggregation level (default: detail)",
552
+ },
553
+ "filters": {
554
+ "type": "object",
555
+ "description": "Filters on data fields to limit which transactions to index. For date filtering, use actual date field names with operators (e.g., {'transaction_timestamp': '>=2023-10-20'})",
556
+ },
557
+ "max_records": {
558
+ "type": "integer",
559
+ "description": "Maximum records to index (up to 10,000 per query)",
560
+ },
561
+ "program": {"type": "string", "description": "Override default program"},
562
+ },
563
+ },
564
+ ),
565
+ Tool(
566
+ name="semantic_search_transactions",
567
+ description=(
568
+ "Search transactions using natural language queries. "
569
+ "Examples: 'coffee shop purchases', 'large transactions over $100', 'gas station visits'. "
570
+ "Returns full transaction data from LOCAL storage (no API calls, no token limits!). "
571
+ "Requires transactions to be synced first (use index_transactions)."
572
+ ),
573
+ inputSchema={
574
+ "type": "object",
575
+ "required": ["query"],
576
+ "properties": {
577
+ "query": {
578
+ "type": "string",
579
+ "description": "Natural language search query (e.g., 'coffee shop purchases')",
580
+ },
581
+ "n_results": {
582
+ "type": "integer",
583
+ "default": 10,
584
+ "description": "Number of results to return (default: 10)",
585
+ },
586
+ "filters": {
587
+ "type": "object",
588
+ "description": "Metadata filters to narrow search (e.g., amount range, date range)",
589
+ },
590
+ "enrich": {
591
+ "type": "boolean",
592
+ "default": True,
593
+ "description": "Include full transaction details from local storage (default: true)",
594
+ },
595
+ },
596
+ },
597
+ ),
598
+ Tool(
599
+ name="query_local_transactions",
600
+ description=(
601
+ "Query transactions directly from local SQLite storage using filters. "
602
+ "No semantic search - just standard filtering (merchant, amount, date, etc.). "
603
+ "Returns FULL transaction data with NO token limits or API calls. "
604
+ "Perfect for LLM analysis of complete datasets. "
605
+ "Supports pagination for large result sets."
606
+ ),
607
+ inputSchema={
608
+ "type": "object",
609
+ "properties": {
610
+ "filters": {
611
+ "type": "object",
612
+ "description": (
613
+ "Filters using field names (e.g., {\"merchant_name\": \"Starbucks\", "
614
+ "\"transaction_amount\": {\">\": 10}}). "
615
+ "Operators: >, <, >=, <=, =, !=, like"
616
+ ),
617
+ },
618
+ "limit": {
619
+ "type": "integer",
620
+ "default": 100,
621
+ "description": "Maximum number of results (default: 100)",
622
+ },
623
+ "offset": {
624
+ "type": "integer",
625
+ "default": 0,
626
+ "description": "Offset for pagination in local SQLite storage (default: 0)",
627
+ },
628
+ "order_by": {
629
+ "type": "string",
630
+ "default": "created_time DESC",
631
+ "description": "SQL ORDER BY clause (default: 'created_time DESC')",
632
+ },
633
+ },
634
+ },
635
+ ),
636
+ Tool(
637
+ name="find_similar_transactions",
638
+ description=(
639
+ "Find transactions similar to a specific transaction. "
640
+ "Useful for finding related purchases, duplicate transactions, or patterns. "
641
+ "Requires the reference transaction to be indexed first."
642
+ ),
643
+ inputSchema={
644
+ "type": "object",
645
+ "required": ["transaction_token"],
646
+ "properties": {
647
+ "transaction_token": {
648
+ "type": "string",
649
+ "description": "Token of the reference transaction",
650
+ },
651
+ "n_results": {
652
+ "type": "integer",
653
+ "default": 10,
654
+ "description": "Number of similar transactions to return (default: 10)",
655
+ },
656
+ "filters": {
657
+ "type": "object",
658
+ "description": "Additional metadata filters",
659
+ },
660
+ },
661
+ },
662
+ ),
663
+ Tool(
664
+ name="get_index_stats",
665
+ description=(
666
+ "Get comprehensive statistics about local storage (SQLite + ChromaDB). "
667
+ "Shows transaction counts, database size, embedding model info, and storage status. "
668
+ "Useful for monitoring local data availability."
669
+ ),
670
+ inputSchema={"type": "object", "properties": {}},
671
+ ),
672
+ Tool(
673
+ name="clear_index",
674
+ description=(
675
+ "Clear all transactions from the vector index. "
676
+ "Use this to reset and start fresh. Cannot be undone."
677
+ ),
678
+ inputSchema={"type": "object", "properties": {}},
679
+ ),
680
+ ]
681
+
682
+ # Build the final TOOLS list dynamically based on feature flags
683
+ TOOLS = BASE_TOOLS.copy()
684
+ if RAG_AVAILABLE:
685
+ TOOLS.extend(RAG_TOOLS)
686
+ print(f"[MCP Server] Registered {len(BASE_TOOLS)} base tools + {len(RAG_TOOLS)} RAG tools = {len(TOOLS)} total tools", file=sys.stderr)
687
+ else:
688
+ print(f"[MCP Server] Registered {len(BASE_TOOLS)} base tools (RAG tools disabled)", file=sys.stderr)
689
+
690
+
691
+ @app.list_tools()
692
+ async def list_tools() -> List[Tool]:
693
+ """List all available tools."""
694
+ return TOOLS
695
+
696
+
697
+ @app.call_tool()
698
+ async def call_tool(name: str, arguments: Any) -> List[TextContent]:
699
+ """Handle tool calls."""
700
+ try:
701
+ client = get_client()
702
+
703
+ # Transaction tools
704
+ if name == "get_authorizations":
705
+ aggregation = arguments.pop("aggregation", "detail")
706
+ result = client.get_view("authorizations", aggregation, **arguments)
707
+ return [TextContent(type="text", text=format_response(result))]
708
+
709
+ elif name == "get_settlements":
710
+ aggregation = arguments.pop("aggregation", "detail")
711
+ result = client.get_view("settlements", aggregation, **arguments)
712
+ return [TextContent(type="text", text=format_response(result))]
713
+
714
+ elif name == "get_clearings":
715
+ aggregation = arguments.pop("aggregation", "detail")
716
+ result = client.get_view("clearing", aggregation, **arguments)
717
+ return [TextContent(type="text", text=format_response(result))]
718
+
719
+ elif name == "get_declines":
720
+ aggregation = arguments.pop("aggregation", "detail")
721
+ result = client.get_view("declines", aggregation, **arguments)
722
+ return [TextContent(type="text", text=format_response(result))]
723
+
724
+ elif name == "get_loads":
725
+ aggregation = arguments.pop("aggregation", "detail")
726
+ result = client.get_view("loads", aggregation, **arguments)
727
+ return [TextContent(type="text", text=format_response(result))]
728
+
729
+ # Financial tools
730
+ elif name == "get_program_balances":
731
+ result = client.get_view("programbalances", "day", **arguments)
732
+ return [TextContent(type="text", text=format_response(result))]
733
+
734
+ elif name == "get_program_balances_settlement":
735
+ result = client.get_view("programbalancessettlement", "day", **arguments)
736
+ return [TextContent(type="text", text=format_response(result))]
737
+
738
+ elif name == "get_activity_balances":
739
+ result = client.get_view("activitybalances", "day", **arguments)
740
+ return [TextContent(type="text", text=format_response(result))]
741
+
742
+ # Card & User tools
743
+ elif name == "get_cards":
744
+ result = client.get_view("cards", "detail", **arguments)
745
+ return [TextContent(type="text", text=format_response(result))]
746
+
747
+ elif name == "get_users":
748
+ result = client.get_view("users", "detail", **arguments)
749
+ return [TextContent(type="text", text=format_response(result))]
750
+
751
+ # Chargeback tools
752
+ elif name == "get_chargebacks_status":
753
+ result = client.get_view("chargebacks", "status", **arguments)
754
+ return [TextContent(type="text", text=format_response(result))]
755
+
756
+ elif name == "get_chargebacks_detail":
757
+ result = client.get_view("chargebacks", "detail", **arguments)
758
+ return [TextContent(type="text", text=format_response(result))]
759
+
760
+ # Metadata tools
761
+ elif name == "list_available_views":
762
+ result = client.get_views_list()
763
+ return [TextContent(type="text", text=format_response(result))]
764
+
765
+ elif name == "get_view_schema":
766
+ view_name = arguments["view_name"]
767
+ aggregation = arguments.pop("aggregation", "detail")
768
+ result = client.get_view_schema(view_name, aggregation)
769
+ return [TextContent(type="text", text=format_response(result))]
770
+
771
+ # Export tool
772
+ elif name == "export_view_to_file":
773
+ view_name = arguments.pop("view_name")
774
+ aggregation = arguments.pop("aggregation", "detail")
775
+ output_path = arguments.pop("output_path")
776
+ format_type = arguments.pop("format", "json")
777
+ max_records = arguments.pop("max_records", None)
778
+
779
+ result = client.export_to_file(
780
+ view_name=view_name,
781
+ aggregation=aggregation,
782
+ output_path=output_path,
783
+ format=format_type,
784
+ max_records=max_records,
785
+ **arguments
786
+ )
787
+ return [TextContent(type="text", text=format_response(result))]
788
+
789
+ # RAG tools
790
+ elif name == "index_transactions":
791
+ if not RAG_AVAILABLE:
792
+ return [TextContent(type="text", text=format_response({
793
+ "error": "Local storage features are not enabled",
794
+ "message": "To use this feature, set ENABLE_LOCAL_STORAGE=true in your environment and install RAG dependencies",
795
+ "install_command": "pip install marqeta-diva-mcp[rag]"
796
+ }))]
797
+
798
+ view_name = arguments.pop("view_name", "authorizations")
799
+ aggregation = arguments.pop("aggregation", "detail")
800
+
801
+ result = rag_tools.index_transactions(
802
+ diva_client=client,
803
+ view_name=view_name,
804
+ aggregation=aggregation,
805
+ **arguments
806
+ )
807
+ return [TextContent(type="text", text=format_response(result))]
808
+
809
+ elif name == "semantic_search_transactions":
810
+ if not RAG_AVAILABLE:
811
+ return [TextContent(type="text", text=format_response({
812
+ "error": "Local storage features are not enabled",
813
+ "message": "To use this feature, set ENABLE_LOCAL_STORAGE=true in your environment and install RAG dependencies",
814
+ "install_command": "pip install marqeta-diva-mcp[rag]"
815
+ }))]
816
+
817
+ query = arguments.pop("query")
818
+ n_results = arguments.pop("n_results", 10)
819
+ filters = arguments.pop("filters", None)
820
+ enrich = arguments.pop("enrich", True)
821
+
822
+ result = rag_tools.semantic_search_transactions(
823
+ diva_client=client,
824
+ query=query,
825
+ n_results=n_results,
826
+ filters=filters,
827
+ enrich=enrich
828
+ )
829
+ return [TextContent(type="text", text=format_response(result))]
830
+
831
+ elif name == "query_local_transactions":
832
+ if not RAG_AVAILABLE:
833
+ return [TextContent(type="text", text=format_response({
834
+ "error": "Local storage features are not enabled",
835
+ "message": "To use this feature, set ENABLE_LOCAL_STORAGE=true in your environment and install RAG dependencies",
836
+ "install_command": "pip install marqeta-diva-mcp[rag]"
837
+ }))]
838
+
839
+ filters = arguments.pop("filters", None)
840
+ limit = arguments.pop("limit", 100)
841
+ offset = arguments.pop("offset", 0)
842
+ order_by = arguments.pop("order_by", "created_time DESC")
843
+
844
+ result = rag_tools.query_local_transactions(
845
+ filters=filters,
846
+ limit=limit,
847
+ offset=offset,
848
+ order_by=order_by
849
+ )
850
+ return [TextContent(type="text", text=format_response(result))]
851
+
852
+ elif name == "find_similar_transactions":
853
+ if not RAG_AVAILABLE:
854
+ return [TextContent(type="text", text=format_response({
855
+ "error": "Local storage features are not enabled",
856
+ "message": "To use this feature, set ENABLE_LOCAL_STORAGE=true in your environment and install RAG dependencies",
857
+ "install_command": "pip install marqeta-diva-mcp[rag]"
858
+ }))]
859
+
860
+ transaction_token = arguments.pop("transaction_token")
861
+ n_results = arguments.pop("n_results", 10)
862
+ filters = arguments.pop("filters", None)
863
+
864
+ result = rag_tools.find_similar_transactions(
865
+ diva_client=client,
866
+ transaction_token=transaction_token,
867
+ n_results=n_results,
868
+ filters=filters
869
+ )
870
+ return [TextContent(type="text", text=format_response(result))]
871
+
872
+ elif name == "get_index_stats":
873
+ if not RAG_AVAILABLE:
874
+ return [TextContent(type="text", text=format_response({
875
+ "error": "Local storage features are not enabled",
876
+ "message": "To use this feature, set ENABLE_LOCAL_STORAGE=true in your environment and install RAG dependencies",
877
+ "install_command": "pip install marqeta-diva-mcp[rag]"
878
+ }))]
879
+
880
+ result = rag_tools.get_index_stats()
881
+ return [TextContent(type="text", text=format_response(result))]
882
+
883
+ elif name == "clear_index":
884
+ if not RAG_AVAILABLE:
885
+ return [TextContent(type="text", text=format_response({
886
+ "error": "Local storage features are not enabled",
887
+ "message": "To use this feature, set ENABLE_LOCAL_STORAGE=true in your environment and install RAG dependencies",
888
+ "install_command": "pip install marqeta-diva-mcp[rag]"
889
+ }))]
890
+
891
+ result = rag_tools.clear_index()
892
+ return [TextContent(type="text", text=format_response(result))]
893
+
894
+ elif name == "get_server_version":
895
+ result = {
896
+ "version": SERVER_VERSION,
897
+ "message": "If you don't see version 0.2.0, the server needs to be restarted",
898
+ "rag_features_enabled": RAG_AVAILABLE,
899
+ "features": {
900
+ "base_tools": f"{len(BASE_TOOLS)} tools available",
901
+ "rag_tools": f"{len(RAG_TOOLS)} tools available" if RAG_AVAILABLE else "disabled (set ENABLE_LOCAL_STORAGE=true to enable)",
902
+ },
903
+ "fixes_included": [
904
+ "Removed non-existent start_date/end_date parameters",
905
+ "Added sys import to fix export errors",
906
+ "Updated to correct 10,000 record limit",
907
+ "Disabled client-side field validation",
908
+ "Fixed filter syntax for date fields",
909
+ "CRITICAL FIX: Changed 'data' to 'records' to match actual API response format",
910
+ "Fixed ChromaDB ID type error - convert integer IDs to strings",
911
+ "Made RAG features optional with ENABLE_LOCAL_STORAGE environment variable"
912
+ ]
913
+ }
914
+ return [TextContent(type="text", text=format_response(result))]
915
+
916
+ else:
917
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
918
+
919
+ except Exception as e:
920
+ error_msg = format_error(e)
921
+ return [TextContent(type="text", text=f"Error: {error_msg}")]
922
+
923
+
924
+ async def async_main():
925
+ """Run the MCP server."""
926
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
927
+ await app.run(
928
+ read_stream,
929
+ write_stream,
930
+ app.create_initialization_options(),
931
+ )
932
+
933
+
934
+ def main():
935
+ """Entry point for the MCP server."""
936
+ asyncio.run(async_main())
937
+
938
+
939
+ if __name__ == "__main__":
940
+ main()