erpnext-mcp 0.2.0__py3-none-any.whl → 0.4.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.
- erpnext_mcp/server.py +210 -22
- {erpnext_mcp-0.2.0.dist-info → erpnext_mcp-0.4.0.dist-info}/METADATA +10 -1
- erpnext_mcp-0.4.0.dist-info/RECORD +9 -0
- erpnext_mcp-0.2.0.dist-info/RECORD +0 -9
- {erpnext_mcp-0.2.0.dist-info → erpnext_mcp-0.4.0.dist-info}/WHEEL +0 -0
- {erpnext_mcp-0.2.0.dist-info → erpnext_mcp-0.4.0.dist-info}/entry_points.txt +0 -0
- {erpnext_mcp-0.2.0.dist-info → erpnext_mcp-0.4.0.dist-info}/licenses/LICENSE +0 -0
erpnext_mcp/server.py
CHANGED
|
@@ -314,29 +314,27 @@ async def get_stock_ledger(item_code: str | None = None, warehouse: str | None =
|
|
|
314
314
|
|
|
315
315
|
|
|
316
316
|
@mcp.tool()
|
|
317
|
-
async def
|
|
318
|
-
|
|
319
|
-
filename: str,
|
|
317
|
+
async def upload_file_from_url(
|
|
318
|
+
file_url: str,
|
|
319
|
+
filename: str | None = None,
|
|
320
320
|
attached_to_doctype: str | None = None,
|
|
321
321
|
attached_to_name: str | None = None,
|
|
322
322
|
is_private: bool = True,
|
|
323
323
|
) -> dict:
|
|
324
|
-
"""Upload a file to ERPNext.
|
|
324
|
+
"""Upload a file to ERPNext from a URL.
|
|
325
325
|
|
|
326
326
|
Args:
|
|
327
|
-
|
|
328
|
-
filename:
|
|
329
|
-
attached_to_doctype: Optional DocType to attach file to
|
|
330
|
-
attached_to_name: Optional document name to attach file to
|
|
327
|
+
file_url: Source URL to fetch the file from
|
|
328
|
+
filename: Optional name for the file (will be inferred from URL if not provided)
|
|
329
|
+
attached_to_doctype: Optional DocType to attach file to
|
|
330
|
+
attached_to_name: Optional document name to attach file to
|
|
331
331
|
is_private: Whether file should be private (default True)
|
|
332
332
|
|
|
333
333
|
Returns:
|
|
334
334
|
File document with file_url and other metadata
|
|
335
335
|
"""
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return await get_client().upload_file(
|
|
339
|
-
file_content=file_content,
|
|
336
|
+
return await get_client().upload_file_from_url(
|
|
337
|
+
file_url=file_url,
|
|
340
338
|
filename=filename,
|
|
341
339
|
attached_to_doctype=attached_to_doctype,
|
|
342
340
|
attached_to_name=attached_to_name,
|
|
@@ -345,28 +343,40 @@ async def upload_file(
|
|
|
345
343
|
|
|
346
344
|
|
|
347
345
|
@mcp.tool()
|
|
348
|
-
async def
|
|
349
|
-
|
|
346
|
+
async def upload_file(
|
|
347
|
+
file_path: str,
|
|
350
348
|
filename: str | None = None,
|
|
351
349
|
attached_to_doctype: str | None = None,
|
|
352
350
|
attached_to_name: str | None = None,
|
|
353
351
|
is_private: bool = True,
|
|
354
352
|
) -> dict:
|
|
355
|
-
"""Upload a file to ERPNext
|
|
353
|
+
"""Upload a local file to ERPNext.
|
|
356
354
|
|
|
357
355
|
Args:
|
|
358
|
-
|
|
359
|
-
filename: Optional name for the file (
|
|
360
|
-
attached_to_doctype: Optional DocType to attach file to
|
|
361
|
-
attached_to_name: Optional document name to attach file to
|
|
356
|
+
file_path: Local file path to upload (e.g. "/mnt/nas/files/report.pdf")
|
|
357
|
+
filename: Optional name for the uploaded file (defaults to original filename)
|
|
358
|
+
attached_to_doctype: Optional DocType to attach file to (e.g. "Project", "Item")
|
|
359
|
+
attached_to_name: Optional document name to attach file to (e.g. "PROJ-0001")
|
|
362
360
|
is_private: Whether file should be private (default True)
|
|
363
361
|
|
|
364
362
|
Returns:
|
|
365
363
|
File document with file_url and other metadata
|
|
366
364
|
"""
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
365
|
+
from pathlib import Path
|
|
366
|
+
|
|
367
|
+
path = Path(file_path)
|
|
368
|
+
if not path.exists():
|
|
369
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
370
|
+
|
|
371
|
+
# 讀取檔案內容
|
|
372
|
+
file_content = path.read_bytes()
|
|
373
|
+
|
|
374
|
+
# 使用原始檔名或指定的檔名
|
|
375
|
+
upload_filename = filename or path.name
|
|
376
|
+
|
|
377
|
+
return await get_client().upload_file(
|
|
378
|
+
file_content=file_content,
|
|
379
|
+
filename=upload_filename,
|
|
370
380
|
attached_to_doctype=attached_to_doctype,
|
|
371
381
|
attached_to_name=attached_to_name,
|
|
372
382
|
is_private=is_private,
|
|
@@ -430,6 +440,184 @@ async def download_file(file_name: str) -> dict:
|
|
|
430
440
|
}
|
|
431
441
|
|
|
432
442
|
|
|
443
|
+
# ── Supplier/Customer Details ──────────────────────────
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@mcp.tool()
|
|
447
|
+
async def get_supplier_details(name: str | None = None, keyword: str | None = None) -> dict:
|
|
448
|
+
"""Get complete supplier details including address, phone, and contacts.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
name: Exact supplier name (e.g. "SF0009-2 - 永心企業社")
|
|
452
|
+
keyword: Search keyword to find supplier (e.g. "永心", "健保局")
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Dict with supplier info, address (phone/fax), and contacts (our purchaser + their contacts)
|
|
456
|
+
"""
|
|
457
|
+
client = get_client()
|
|
458
|
+
|
|
459
|
+
# Find supplier
|
|
460
|
+
if name:
|
|
461
|
+
supplier = await client.get_doc("Supplier", name)
|
|
462
|
+
elif keyword:
|
|
463
|
+
# 先搜尋 name 欄位
|
|
464
|
+
suppliers = await client.get_list(
|
|
465
|
+
"Supplier",
|
|
466
|
+
fields=["name", "supplier_name", "supplier_group", "country", "custom_alias"],
|
|
467
|
+
filters={"name": ["like", f"%{keyword}%"]},
|
|
468
|
+
limit_page_length=1,
|
|
469
|
+
)
|
|
470
|
+
# 找不到則搜尋 custom_alias 欄位(別名)
|
|
471
|
+
if not suppliers:
|
|
472
|
+
suppliers = await client.get_list(
|
|
473
|
+
"Supplier",
|
|
474
|
+
fields=["name", "supplier_name", "supplier_group", "country", "custom_alias"],
|
|
475
|
+
filters={"custom_alias": ["like", f"%{keyword}%"]},
|
|
476
|
+
limit_page_length=1,
|
|
477
|
+
)
|
|
478
|
+
if not suppliers:
|
|
479
|
+
return {"error": f"找不到關鍵字「{keyword}」的供應商"}
|
|
480
|
+
supplier = await client.get_doc("Supplier", suppliers[0]["name"])
|
|
481
|
+
else:
|
|
482
|
+
return {"error": "請提供 name 或 keyword"}
|
|
483
|
+
|
|
484
|
+
supplier_name = supplier.get("name")
|
|
485
|
+
|
|
486
|
+
# Get address (phone/fax)
|
|
487
|
+
# Address title format: "代碼 地址", e.g. "SF0009-2 地址"
|
|
488
|
+
code = supplier_name.split(" - ")[0] if " - " in supplier_name else supplier_name
|
|
489
|
+
addresses = await client.get_list(
|
|
490
|
+
"Address",
|
|
491
|
+
fields=["address_title", "address_line1", "city", "pincode", "phone", "fax"],
|
|
492
|
+
filters={"address_title": ["like", f"%{code}%"]},
|
|
493
|
+
limit_page_length=5,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Get contacts via Dynamic Link
|
|
497
|
+
contacts = await client.get_list(
|
|
498
|
+
"Contact",
|
|
499
|
+
fields=["name", "first_name", "designation", "phone", "mobile_no", "email_id"],
|
|
500
|
+
filters=[["Dynamic Link", "link_name", "=", supplier_name]],
|
|
501
|
+
limit_page_length=50,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Categorize contacts
|
|
505
|
+
# 有 designation 的是我們的人(採購人員/業務人員),沒有的是對方的聯絡人
|
|
506
|
+
our_contacts = []
|
|
507
|
+
their_contacts = []
|
|
508
|
+
for c in contacts:
|
|
509
|
+
contact_info = {
|
|
510
|
+
"name": c.get("first_name") or c.get("name"),
|
|
511
|
+
"designation": c.get("designation") or "",
|
|
512
|
+
"phone": c.get("phone") or c.get("mobile_no") or "",
|
|
513
|
+
"email": c.get("email_id") or "",
|
|
514
|
+
}
|
|
515
|
+
if c.get("designation"):
|
|
516
|
+
our_contacts.append(contact_info)
|
|
517
|
+
else:
|
|
518
|
+
their_contacts.append(contact_info)
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
"supplier": {
|
|
522
|
+
"name": supplier_name,
|
|
523
|
+
"alias": supplier.get("custom_alias") or "",
|
|
524
|
+
"group": supplier.get("supplier_group"),
|
|
525
|
+
"country": supplier.get("country"),
|
|
526
|
+
"currency": supplier.get("default_currency"),
|
|
527
|
+
},
|
|
528
|
+
"address": addresses[0] if addresses else None,
|
|
529
|
+
"our_contacts": our_contacts,
|
|
530
|
+
"their_contacts": their_contacts,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@mcp.tool()
|
|
535
|
+
async def get_customer_details(name: str | None = None, keyword: str | None = None) -> dict:
|
|
536
|
+
"""Get complete customer details including address, phone, and contacts.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
name: Exact customer name (e.g. "CM0001 - 正達工程股份有限公司")
|
|
540
|
+
keyword: Search keyword to find customer (e.g. "正達")
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Dict with customer info, address (phone/fax), and contacts (our sales + their contacts)
|
|
544
|
+
"""
|
|
545
|
+
client = get_client()
|
|
546
|
+
|
|
547
|
+
# Find customer
|
|
548
|
+
if name:
|
|
549
|
+
customer = await client.get_doc("Customer", name)
|
|
550
|
+
elif keyword:
|
|
551
|
+
# 先搜尋 name 欄位
|
|
552
|
+
customers = await client.get_list(
|
|
553
|
+
"Customer",
|
|
554
|
+
fields=["name", "customer_name", "customer_group", "territory", "custom_alias"],
|
|
555
|
+
filters={"name": ["like", f"%{keyword}%"]},
|
|
556
|
+
limit_page_length=1,
|
|
557
|
+
)
|
|
558
|
+
# 找不到則搜尋 custom_alias 欄位(別名)
|
|
559
|
+
if not customers:
|
|
560
|
+
customers = await client.get_list(
|
|
561
|
+
"Customer",
|
|
562
|
+
fields=["name", "customer_name", "customer_group", "territory", "custom_alias"],
|
|
563
|
+
filters={"custom_alias": ["like", f"%{keyword}%"]},
|
|
564
|
+
limit_page_length=1,
|
|
565
|
+
)
|
|
566
|
+
if not customers:
|
|
567
|
+
return {"error": f"找不到關鍵字「{keyword}」的客戶"}
|
|
568
|
+
customer = await client.get_doc("Customer", customers[0]["name"])
|
|
569
|
+
else:
|
|
570
|
+
return {"error": "請提供 name 或 keyword"}
|
|
571
|
+
|
|
572
|
+
customer_name = customer.get("name")
|
|
573
|
+
|
|
574
|
+
# Get address (phone/fax)
|
|
575
|
+
code = customer_name.split(" - ")[0] if " - " in customer_name else customer_name
|
|
576
|
+
addresses = await client.get_list(
|
|
577
|
+
"Address",
|
|
578
|
+
fields=["address_title", "address_line1", "city", "pincode", "phone", "fax"],
|
|
579
|
+
filters={"address_title": ["like", f"%{code}%"]},
|
|
580
|
+
limit_page_length=5,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Get contacts via Dynamic Link
|
|
584
|
+
contacts = await client.get_list(
|
|
585
|
+
"Contact",
|
|
586
|
+
fields=["name", "first_name", "designation", "phone", "mobile_no", "email_id"],
|
|
587
|
+
filters=[["Dynamic Link", "link_name", "=", customer_name]],
|
|
588
|
+
limit_page_length=50,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Categorize contacts
|
|
592
|
+
# 有 designation 的是我們的人(採購人員/業務人員),沒有的是對方的聯絡人
|
|
593
|
+
our_contacts = []
|
|
594
|
+
their_contacts = []
|
|
595
|
+
for c in contacts:
|
|
596
|
+
contact_info = {
|
|
597
|
+
"name": c.get("first_name") or c.get("name"),
|
|
598
|
+
"designation": c.get("designation") or "",
|
|
599
|
+
"phone": c.get("phone") or c.get("mobile_no") or "",
|
|
600
|
+
"email": c.get("email_id") or "",
|
|
601
|
+
}
|
|
602
|
+
if c.get("designation"):
|
|
603
|
+
our_contacts.append(contact_info)
|
|
604
|
+
else:
|
|
605
|
+
their_contacts.append(contact_info)
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
"customer": {
|
|
609
|
+
"name": customer_name,
|
|
610
|
+
"alias": customer.get("custom_alias") or "",
|
|
611
|
+
"group": customer.get("customer_group"),
|
|
612
|
+
"territory": customer.get("territory"),
|
|
613
|
+
"currency": customer.get("default_currency"),
|
|
614
|
+
},
|
|
615
|
+
"address": addresses[0] if addresses else None,
|
|
616
|
+
"our_contacts": our_contacts,
|
|
617
|
+
"their_contacts": their_contacts,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
433
621
|
def main():
|
|
434
622
|
mcp.run()
|
|
435
623
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: erpnext-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: MCP Server for ERPNext REST API
|
|
5
5
|
Project-URL: Homepage, https://github.com/ching-tech/erpnext-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/ching-tech/erpnext-mcp
|
|
@@ -57,6 +57,8 @@ MCP (Model Context Protocol) server for ERPNext REST API, built with [FastMCP](h
|
|
|
57
57
|
- **Schema** — Inspect DocType field definitions, list all DocTypes
|
|
58
58
|
- **Inventory** — Stock balance, stock ledger, item prices
|
|
59
59
|
- **Trading** — Document conversion (e.g. Quotation → Sales Order), party balance
|
|
60
|
+
- **Supplier/Customer** — Get complete details with address, phone, contacts; supports alias search
|
|
61
|
+
- **Files** — Upload, list, download files
|
|
60
62
|
- **Helpers** — Link search (autocomplete), document count, generic method calls
|
|
61
63
|
|
|
62
64
|
## Requirements
|
|
@@ -111,6 +113,13 @@ set -a && source .env && set +a && uv run erpnext-mcp
|
|
|
111
113
|
| `get_item_price` | Item prices from price lists |
|
|
112
114
|
| `make_mapped_doc` | Document conversion (e.g. SO → DN) |
|
|
113
115
|
| `get_party_balance` | Outstanding balance for Customer/Supplier |
|
|
116
|
+
| `get_supplier_details` | Get supplier with address, phone, contacts (supports alias search) |
|
|
117
|
+
| `get_customer_details` | Get customer with address, phone, contacts (supports alias search) |
|
|
118
|
+
| `upload_file` | Upload a local file to ERPNext (by file path) |
|
|
119
|
+
| `upload_file_from_url` | Upload a file from URL |
|
|
120
|
+
| `list_files` | List files attached to a document |
|
|
121
|
+
| `download_file` | Download a file by URL |
|
|
122
|
+
| `get_file_url` | Get download URL for a file |
|
|
114
123
|
|
|
115
124
|
## MCP Client Configuration
|
|
116
125
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
erpnext_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
erpnext_mcp/client.py,sha256=wX71XIi6DO2tVyKcP82NIiC50CE-BxqjglAraERPjsA,13582
|
|
3
|
+
erpnext_mcp/server.py,sha256=am7xtqeOJgX5SN4oekz3Swh-6h3qsArlnr3StqEH9vw,21009
|
|
4
|
+
erpnext_mcp/types.py,sha256=MC7H3f1BgpvIbWDwXSv1uFwH8nTD0BMiiqnHypsutk8,643
|
|
5
|
+
erpnext_mcp-0.4.0.dist-info/METADATA,sha256=b2digpWpo71sbQp5JxNArSLtWebRkQ81DPzgYgxy4_I,5723
|
|
6
|
+
erpnext_mcp-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
erpnext_mcp-0.4.0.dist-info/entry_points.txt,sha256=AbRXtSJhFq6PLmqKjNYkFjcKFjQwvkiTI8QAhqqAsps,56
|
|
8
|
+
erpnext_mcp-0.4.0.dist-info/licenses/LICENSE,sha256=M_cHb60t6PxhBL25Y_hbvWe8WaUxG1y5xh5qoWNIlYU,1067
|
|
9
|
+
erpnext_mcp-0.4.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
erpnext_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
erpnext_mcp/client.py,sha256=wX71XIi6DO2tVyKcP82NIiC50CE-BxqjglAraERPjsA,13582
|
|
3
|
-
erpnext_mcp/server.py,sha256=VMUZHv5dqnnvVqf3TON-Bxp-YpsFsN0hW42as2T0bHk,14081
|
|
4
|
-
erpnext_mcp/types.py,sha256=MC7H3f1BgpvIbWDwXSv1uFwH8nTD0BMiiqnHypsutk8,643
|
|
5
|
-
erpnext_mcp-0.2.0.dist-info/METADATA,sha256=fzQIIru7dn_k8kmvkBVS-CaSB2oAgyvAx-REXvHiNVs,5119
|
|
6
|
-
erpnext_mcp-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
erpnext_mcp-0.2.0.dist-info/entry_points.txt,sha256=AbRXtSJhFq6PLmqKjNYkFjcKFjQwvkiTI8QAhqqAsps,56
|
|
8
|
-
erpnext_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=M_cHb60t6PxhBL25Y_hbvWe8WaUxG1y5xh5qoWNIlYU,1067
|
|
9
|
-
erpnext_mcp-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|