erpnext-mcp 0.3.0__py3-none-any.whl → 0.4.1__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/client.py +4 -1
- erpnext_mcp/server.py +55 -25
- {erpnext_mcp-0.3.0.dist-info → erpnext_mcp-0.4.1.dist-info}/METADATA +10 -1
- erpnext_mcp-0.4.1.dist-info/RECORD +9 -0
- erpnext_mcp-0.3.0.dist-info/RECORD +0 -9
- {erpnext_mcp-0.3.0.dist-info → erpnext_mcp-0.4.1.dist-info}/WHEEL +0 -0
- {erpnext_mcp-0.3.0.dist-info → erpnext_mcp-0.4.1.dist-info}/entry_points.txt +0 -0
- {erpnext_mcp-0.3.0.dist-info → erpnext_mcp-0.4.1.dist-info}/licenses/LICENSE +0 -0
erpnext_mcp/client.py
CHANGED
|
@@ -248,7 +248,10 @@ class ERPNextClient:
|
|
|
248
248
|
"/api/method/upload_file",
|
|
249
249
|
files=files,
|
|
250
250
|
data=data,
|
|
251
|
-
headers={
|
|
251
|
+
headers={
|
|
252
|
+
"Authorization": self.headers["Authorization"],
|
|
253
|
+
"Expect": "", # 禁用 100-continue,避免 417 錯誤
|
|
254
|
+
},
|
|
252
255
|
)
|
|
253
256
|
resp.raise_for_status()
|
|
254
257
|
result = resp.json()
|
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,
|
|
@@ -439,7 +449,7 @@ async def get_supplier_details(name: str | None = None, keyword: str | None = No
|
|
|
439
449
|
|
|
440
450
|
Args:
|
|
441
451
|
name: Exact supplier name (e.g. "SF0009-2 - 永心企業社")
|
|
442
|
-
keyword: Search keyword to find supplier (e.g. "永心")
|
|
452
|
+
keyword: Search keyword to find supplier (e.g. "永心", "健保局")
|
|
443
453
|
|
|
444
454
|
Returns:
|
|
445
455
|
Dict with supplier info, address (phone/fax), and contacts (our purchaser + their contacts)
|
|
@@ -450,12 +460,21 @@ async def get_supplier_details(name: str | None = None, keyword: str | None = No
|
|
|
450
460
|
if name:
|
|
451
461
|
supplier = await client.get_doc("Supplier", name)
|
|
452
462
|
elif keyword:
|
|
463
|
+
# 先搜尋 name 欄位
|
|
453
464
|
suppliers = await client.get_list(
|
|
454
465
|
"Supplier",
|
|
455
|
-
fields=["name", "supplier_name", "supplier_group", "country"],
|
|
466
|
+
fields=["name", "supplier_name", "supplier_group", "country", "custom_alias"],
|
|
456
467
|
filters={"name": ["like", f"%{keyword}%"]},
|
|
457
468
|
limit_page_length=1,
|
|
458
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
|
+
)
|
|
459
478
|
if not suppliers:
|
|
460
479
|
return {"error": f"找不到關鍵字「{keyword}」的供應商"}
|
|
461
480
|
supplier = await client.get_doc("Supplier", suppliers[0]["name"])
|
|
@@ -501,6 +520,7 @@ async def get_supplier_details(name: str | None = None, keyword: str | None = No
|
|
|
501
520
|
return {
|
|
502
521
|
"supplier": {
|
|
503
522
|
"name": supplier_name,
|
|
523
|
+
"alias": supplier.get("custom_alias") or "",
|
|
504
524
|
"group": supplier.get("supplier_group"),
|
|
505
525
|
"country": supplier.get("country"),
|
|
506
526
|
"currency": supplier.get("default_currency"),
|
|
@@ -528,12 +548,21 @@ async def get_customer_details(name: str | None = None, keyword: str | None = No
|
|
|
528
548
|
if name:
|
|
529
549
|
customer = await client.get_doc("Customer", name)
|
|
530
550
|
elif keyword:
|
|
551
|
+
# 先搜尋 name 欄位
|
|
531
552
|
customers = await client.get_list(
|
|
532
553
|
"Customer",
|
|
533
|
-
fields=["name", "customer_name", "customer_group", "territory"],
|
|
554
|
+
fields=["name", "customer_name", "customer_group", "territory", "custom_alias"],
|
|
534
555
|
filters={"name": ["like", f"%{keyword}%"]},
|
|
535
556
|
limit_page_length=1,
|
|
536
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
|
+
)
|
|
537
566
|
if not customers:
|
|
538
567
|
return {"error": f"找不到關鍵字「{keyword}」的客戶"}
|
|
539
568
|
customer = await client.get_doc("Customer", customers[0]["name"])
|
|
@@ -578,6 +607,7 @@ async def get_customer_details(name: str | None = None, keyword: str | None = No
|
|
|
578
607
|
return {
|
|
579
608
|
"customer": {
|
|
580
609
|
"name": customer_name,
|
|
610
|
+
"alias": customer.get("custom_alias") or "",
|
|
581
611
|
"group": customer.get("customer_group"),
|
|
582
612
|
"territory": customer.get("territory"),
|
|
583
613
|
"currency": customer.get("default_currency"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: erpnext-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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=3R08zvNnDNf7TJxx7B4EmHYqdXWpucks3hc_NrwrIEM,13698
|
|
3
|
+
erpnext_mcp/server.py,sha256=am7xtqeOJgX5SN4oekz3Swh-6h3qsArlnr3StqEH9vw,21009
|
|
4
|
+
erpnext_mcp/types.py,sha256=MC7H3f1BgpvIbWDwXSv1uFwH8nTD0BMiiqnHypsutk8,643
|
|
5
|
+
erpnext_mcp-0.4.1.dist-info/METADATA,sha256=mAdfUPF_ArLkbxgYY_vl1Z5SxQVJ4267U-_U5juuENs,5723
|
|
6
|
+
erpnext_mcp-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
erpnext_mcp-0.4.1.dist-info/entry_points.txt,sha256=AbRXtSJhFq6PLmqKjNYkFjcKFjQwvkiTI8QAhqqAsps,56
|
|
8
|
+
erpnext_mcp-0.4.1.dist-info/licenses/LICENSE,sha256=M_cHb60t6PxhBL25Y_hbvWe8WaUxG1y5xh5qoWNIlYU,1067
|
|
9
|
+
erpnext_mcp-0.4.1.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=e7Zymn6kQ-Vd3Mtu6c1gRtKcsb_gLLl3DL_e1t1MArw,19757
|
|
4
|
-
erpnext_mcp/types.py,sha256=MC7H3f1BgpvIbWDwXSv1uFwH8nTD0BMiiqnHypsutk8,643
|
|
5
|
-
erpnext_mcp-0.3.0.dist-info/METADATA,sha256=xdLBhOwN7eVB5ngCexvN_OJISuisg_MgigCY_v_VxKg,5119
|
|
6
|
-
erpnext_mcp-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
erpnext_mcp-0.3.0.dist-info/entry_points.txt,sha256=AbRXtSJhFq6PLmqKjNYkFjcKFjQwvkiTI8QAhqqAsps,56
|
|
8
|
-
erpnext_mcp-0.3.0.dist-info/licenses/LICENSE,sha256=M_cHb60t6PxhBL25Y_hbvWe8WaUxG1y5xh5qoWNIlYU,1067
|
|
9
|
-
erpnext_mcp-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|