erpnext-mcp 0.3.0__tar.gz → 0.4.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: erpnext-mcp
3
- Version: 0.3.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
 
@@ -10,6 +10,8 @@ MCP (Model Context Protocol) server for ERPNext REST API, built with [FastMCP](h
10
10
  - **Schema** — Inspect DocType field definitions, list all DocTypes
11
11
  - **Inventory** — Stock balance, stock ledger, item prices
12
12
  - **Trading** — Document conversion (e.g. Quotation → Sales Order), party balance
13
+ - **Supplier/Customer** — Get complete details with address, phone, contacts; supports alias search
14
+ - **Files** — Upload, list, download files
13
15
  - **Helpers** — Link search (autocomplete), document count, generic method calls
14
16
 
15
17
  ## Requirements
@@ -64,6 +66,13 @@ set -a && source .env && set +a && uv run erpnext-mcp
64
66
  | `get_item_price` | Item prices from price lists |
65
67
  | `make_mapped_doc` | Document conversion (e.g. SO → DN) |
66
68
  | `get_party_balance` | Outstanding balance for Customer/Supplier |
69
+ | `get_supplier_details` | Get supplier with address, phone, contacts (supports alias search) |
70
+ | `get_customer_details` | Get customer with address, phone, contacts (supports alias search) |
71
+ | `upload_file` | Upload a local file to ERPNext (by file path) |
72
+ | `upload_file_from_url` | Upload a file from URL |
73
+ | `list_files` | List files attached to a document |
74
+ | `download_file` | Download a file by URL |
75
+ | `get_file_url` | Get download URL for a file |
67
76
 
68
77
  ## MCP Client Configuration
69
78
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "erpnext-mcp"
3
- version = "0.3.0"
3
+ version = "0.4.1"
4
4
  description = "MCP Server for ERPNext REST API"
5
5
  readme = "README.md"
6
6
  license = {file = "LICENSE"}
@@ -248,7 +248,10 @@ class ERPNextClient:
248
248
  "/api/method/upload_file",
249
249
  files=files,
250
250
  data=data,
251
- headers={"Authorization": self.headers["Authorization"]},
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()
@@ -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 upload_file(
318
- file_content_base64: str,
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
- file_content_base64: File content encoded as base64 string
328
- filename: Name for the uploaded file (e.g. "report.pdf")
329
- attached_to_doctype: Optional DocType to attach file to (e.g. "Project", "Item")
330
- attached_to_name: Optional document name to attach file to (e.g. "PROJ-0001")
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
- import base64
337
- file_content = base64.b64decode(file_content_base64)
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 upload_file_from_url(
349
- file_url: str,
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 from a URL.
353
+ """Upload a local file to ERPNext.
356
354
 
357
355
  Args:
358
- file_url: Source URL to fetch the file from
359
- filename: Optional name for the file (will be inferred from URL if not provided)
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
- return await get_client().upload_file_from_url(
368
- file_url=file_url,
369
- filename=filename,
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"),
File without changes
File without changes
File without changes
File without changes
File without changes