lambda-erp 0.6.11__tar.gz → 0.6.13__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.
Files changed (84) hide show
  1. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/PKG-INFO +2 -2
  2. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/README.md +1 -1
  3. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/main.py +2 -1
  4. lambda_erp-0.6.13/api/routers/mcp.py +164 -0
  5. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/pyproject.toml +1 -1
  6. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/.gitignore +0 -0
  7. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/LICENSE +0 -0
  8. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/__init__.py +0 -0
  9. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/attachments.py +0 -0
  10. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/auth.py +0 -0
  11. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/chat.py +0 -0
  13. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/deps.py +0 -0
  15. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/errors.py +0 -0
  16. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/oauth.py +0 -0
  17. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/pdf.py +0 -0
  18. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/providers.py +0 -0
  19. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/remarks_md.py +0 -0
  20. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/admin.py +0 -0
  23. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/analytics.py +0 -0
  24. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/bank_reconciliation.py +0 -0
  25. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/chat_api.py +0 -0
  26. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/documents.py +0 -0
  27. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/masters.py +0 -0
  28. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/proposals.py +0 -0
  29. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/reports.py +0 -0
  30. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/routers/setup.py +0 -0
  31. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/services.py +0 -0
  32. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/templates/document.html +0 -0
  33. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/api/templates/proposal.html +0 -0
  34. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/docs/agents/README.md +0 -0
  35. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/frontend/LICENSE +0 -0
  36. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/frontend/README.md +0 -0
  37. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/frontend/src/api/client.ts +0 -0
  38. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/__init__.py +0 -0
  39. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/__init__.py +0 -0
  40. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/bank_transaction.py +0 -0
  41. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/budget.py +0 -0
  42. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  43. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/journal_entry.py +0 -0
  45. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/payment_entry.py +0 -0
  46. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/pos_invoice.py +0 -0
  47. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/purchase_invoice.py +0 -0
  48. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/revaluation.py +0 -0
  49. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/sales_invoice.py +0 -0
  50. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/__init__.py +0 -0
  51. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/engine.py +0 -0
  52. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/pack.py +0 -0
  53. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  54. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  55. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  56. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  57. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  58. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  59. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/profiles.py +0 -0
  60. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/setup/spine.py +0 -0
  61. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/accounting/subscription.py +0 -0
  62. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/buying/__init__.py +0 -0
  63. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/buying/purchase_order.py +0 -0
  64. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/__init__.py +0 -0
  65. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/currency.py +0 -0
  66. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/defaults.py +0 -0
  67. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/pricing_rule.py +0 -0
  68. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  69. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/database.py +0 -0
  70. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/exceptions.py +0 -0
  71. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/hooks.py +0 -0
  72. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/model.py +0 -0
  73. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/__init__.py +0 -0
  74. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/proposal.py +0 -0
  75. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/quotation.py +0 -0
  76. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/selling/sales_order.py +0 -0
  77. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/simulation.py +0 -0
  78. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/__init__.py +0 -0
  79. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/delivery_note.py +0 -0
  80. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/purchase_receipt.py +0 -0
  81. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/stock_entry.py +0 -0
  82. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/stock/stock_ledger.py +0 -0
  83. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/lambda_erp/utils.py +0 -0
  84. {lambda_erp-0.6.11 → lambda_erp-0.6.13}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.6.11
3
+ Version: 0.6.13
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -161,12 +161,12 @@ The result is booked in one step — chart of accounts, sensible default account
161
161
  - Server-side aggregation tool for in-chat factual answers across large datasets
162
162
  - PDF / image attachment → add invoices, create quotations, etc. all directly by adding them in the chat
163
163
  - **Programmatic API** — drive the ERP from external systems with per-user, role-capped Bearer keys: a [chat API](docs/chat-api.md) (converse with the agent) and a [REST API](docs/rest-api.md) (documents, masters, reports — the same endpoints the web app uses, so a connector or sync job can create and submit documents directly). Both off by default; an admin enables each independently and issues keys.
164
+ - **MCP server** — the ERP speaks the [Model Context Protocol](https://modelcontextprotocol.io) at `POST /api/mcp`, so **any MCP-capable agent (Claude, Codex, an IDE assistant) can drive it directly** — list/create/update/submit documents, manage master data, run reports — as *structured, validated tool calls*, not brittle screen-scraping or hand-written HTTP glue. The tool list is built from the live registries, so a plugin's own doctypes (e.g. a CRM's leads/contacts) are exposed automatically. It reuses the *same* Bearer keys and roles as the REST API — a viewer key is read-only, a manager key can write — so access is scoped, auditable, and off by default. In practice: point your assistant at your instance and ask "which invoices are overdue?" or "create a quotation for Acme with 3× SVC-SPARK" and it operates on real data, from wherever you already work. The API-keys settings page hands you ready-to-paste Claude and Codex config the moment you create a key.
164
165
  - Auth with admin/manager/viewer roles plus a public demo mode
165
166
  - Full test suite that exercises every cycle against an in-memory SQLite
166
167
 
167
168
  ## What's still todo (Suggestions welcome)
168
169
 
169
- - MCP integration for supplier/customer communication (quotes, orders, confirmations)
170
170
  - Multi-currency beyond the simplified current handling
171
171
  - Workflows / approval chains
172
172
  - Serial & batch tracking
@@ -134,12 +134,12 @@ The result is booked in one step — chart of accounts, sensible default account
134
134
  - Server-side aggregation tool for in-chat factual answers across large datasets
135
135
  - PDF / image attachment → add invoices, create quotations, etc. all directly by adding them in the chat
136
136
  - **Programmatic API** — drive the ERP from external systems with per-user, role-capped Bearer keys: a [chat API](docs/chat-api.md) (converse with the agent) and a [REST API](docs/rest-api.md) (documents, masters, reports — the same endpoints the web app uses, so a connector or sync job can create and submit documents directly). Both off by default; an admin enables each independently and issues keys.
137
+ - **MCP server** — the ERP speaks the [Model Context Protocol](https://modelcontextprotocol.io) at `POST /api/mcp`, so **any MCP-capable agent (Claude, Codex, an IDE assistant) can drive it directly** — list/create/update/submit documents, manage master data, run reports — as *structured, validated tool calls*, not brittle screen-scraping or hand-written HTTP glue. The tool list is built from the live registries, so a plugin's own doctypes (e.g. a CRM's leads/contacts) are exposed automatically. It reuses the *same* Bearer keys and roles as the REST API — a viewer key is read-only, a manager key can write — so access is scoped, auditable, and off by default. In practice: point your assistant at your instance and ask "which invoices are overdue?" or "create a quotation for Acme with 3× SVC-SPARK" and it operates on real data, from wherever you already work. The API-keys settings page hands you ready-to-paste Claude and Codex config the moment you create a key.
137
138
  - Auth with admin/manager/viewer roles plus a public demo mode
138
139
  - Full test suite that exercises every cycle against an in-memory SQLite
139
140
 
140
141
  ## What's still todo (Suggestions welcome)
141
142
 
142
- - MCP integration for supplier/customer communication (quotes, orders, confirmations)
143
143
  - Multi-currency beyond the simplified current handling
144
144
  - Workflows / approval chains
145
145
  - Serial & batch tracking
@@ -19,7 +19,7 @@ from api.auth import router as auth_router, COOKIE_NAME, decode_token
19
19
  from api.oauth import router as oauth_router
20
20
  from api.attachments import router as attachments_router
21
21
  from api.chat import chat_websocket, router as chat_router
22
- from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api
22
+ from api.routers import admin, documents, masters, reports, setup as setup_router, bank_reconciliation, analytics, accounting, proposals, chat_api, mcp
23
23
 
24
24
 
25
25
  def load_plugins() -> None:
@@ -114,6 +114,7 @@ app.include_router(accounting.router, prefix="/api")
114
114
  app.include_router(admin.router, prefix="/api")
115
115
  app.include_router(chat_router, prefix="/api")
116
116
  app.include_router(chat_api.router, prefix="/api")
117
+ app.include_router(mcp.router, prefix="/api")
117
118
 
118
119
 
119
120
  @app.get("/api/health")
@@ -0,0 +1,164 @@
1
+ """MCP (Model Context Protocol) endpoint — the fine-grained ERP tool surface for
2
+ LLM agents (Claude, Codex, …) over MCP's Streamable HTTP transport.
3
+
4
+ Reuses everything the chat already has, so there's almost no new logic:
5
+ * schemas — build_tools() (the live, plugin-widened tool list).
6
+ * execution — the same TOOL_HANDLERS (which run validate()).
7
+ * auth — get_current_user: a Bearer API key acts AS its user at the key's
8
+ role, exactly like the REST API, gated by the same
9
+ `rest_api_enabled` Settings flag. No separate MCP credential.
10
+
11
+ Because the tool schemas come from the live registries, a plugin that registers
12
+ doctypes/masters (e.g. the internal CRM's lead/contact/activity) is exposed here
13
+ automatically — the MCP surface is modular by construction.
14
+
15
+ Transport: a single POST /api/mcp speaking JSON-RPC 2.0 (request → JSON result;
16
+ notifications → 202, no body). That's the minimum a tool-only server needs.
17
+ """
18
+ import json
19
+
20
+ from fastapi import APIRouter, HTTPException, Request
21
+ from fastapi.responses import JSONResponse, Response
22
+
23
+ from lambda_erp import get_app_version
24
+ from api.auth import get_current_user
25
+ from api import chat as chat_mod
26
+ from api.chat import TOOL_HANDLERS, build_tools
27
+
28
+ router = APIRouter(tags=["mcp"])
29
+
30
+ PROTOCOL_VERSION = "2025-06-18"
31
+
32
+ # Chat-session-only tools have no meaning without a chat session — keep them out
33
+ # of the MCP surface (an MCP client owns its own context).
34
+ _EXCLUDE = {
35
+ "retrieve_chat_history",
36
+ "list_chat_attachments",
37
+ "retrieve_chat_attachment",
38
+ "create_custom_analytics_report",
39
+ "get_custom_analytics_report",
40
+ "update_custom_analytics_report",
41
+ "plan_company_setup",
42
+ "apply_company_setup",
43
+ }
44
+ # Mirror the REST permission model: reads are viewer+, writes are manager+,
45
+ # delete_master is admin-only (the handler also re-checks).
46
+ _WRITE = {
47
+ "create_document", "update_document", "submit_document", "cancel_document",
48
+ "discard_document", "convert_document", "create_master", "update_master",
49
+ }
50
+ _ADMIN = {"delete_master"}
51
+
52
+
53
+ def _can_write(role) -> bool:
54
+ return role in ("manager", "admin", "public_manager")
55
+
56
+
57
+ def _allowed(name: str, role) -> bool:
58
+ if name in _EXCLUDE:
59
+ return False
60
+ if name in _ADMIN:
61
+ return role == "admin"
62
+ if name in _WRITE:
63
+ return _can_write(role)
64
+ return True
65
+
66
+
67
+ def _require_caller(request: Request) -> dict:
68
+ """A valid Bearer API key is mandatory for MCP — no cookie/public fallback.
69
+ get_current_user then validates the key and applies `rest_api_enabled`."""
70
+ auth = request.headers.get("authorization", "")
71
+ if not auth.lower().startswith("bearer "):
72
+ raise HTTPException(status_code=401, detail="MCP requires a Bearer API key")
73
+ return get_current_user(request)
74
+
75
+
76
+ def _tools(role) -> list:
77
+ out = []
78
+ for tool in build_tools():
79
+ fn = tool["function"]
80
+ if not _allowed(fn["name"], role):
81
+ continue
82
+ out.append({
83
+ "name": fn["name"],
84
+ "description": fn.get("description", ""),
85
+ "inputSchema": fn.get("parameters") or {"type": "object", "properties": {}},
86
+ })
87
+ return out
88
+
89
+
90
+ def _call(name: str, args: dict, user: dict):
91
+ role = user.get("role")
92
+ if not _allowed(name, role):
93
+ return {"error": f"'{name}' is not available to a {role or 'viewer'} key."}
94
+ handlers = dict(TOOL_HANDLERS)
95
+ # delete_master needs the caller's role (admin-only); handled by the chat's
96
+ # role-aware variant.
97
+ handlers["delete_master"] = lambda a: chat_mod._handle_delete_master(a, user)
98
+ handler = handlers.get(name)
99
+ if handler is None:
100
+ raise KeyError(name)
101
+ return handler(args or {})
102
+
103
+
104
+ def _rpc_error(mid, code: int, message: str) -> dict:
105
+ return {"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}}
106
+
107
+
108
+ def _handle(msg: dict, user: dict):
109
+ """Handle one JSON-RPC message. Returns a response dict, or None for a
110
+ notification (no `id`)."""
111
+ method = msg.get("method")
112
+ mid = msg.get("id")
113
+ is_notification = "id" not in msg
114
+
115
+ def result(payload):
116
+ return None if is_notification else {"jsonrpc": "2.0", "id": mid, "result": payload}
117
+
118
+ if method == "initialize":
119
+ return result({
120
+ "protocolVersion": PROTOCOL_VERSION,
121
+ "capabilities": {"tools": {"listChanged": False}},
122
+ "serverInfo": {"name": "lambda-erp", "version": get_app_version()},
123
+ })
124
+ if method in ("notifications/initialized", "notifications/cancelled"):
125
+ return None
126
+ if method == "ping":
127
+ return result({})
128
+ if method == "tools/list":
129
+ return result({"tools": _tools(user.get("role"))})
130
+ if method == "tools/call":
131
+ params = msg.get("params") or {}
132
+ name = params.get("name")
133
+ try:
134
+ out = _call(name, params.get("arguments") or {}, user)
135
+ except KeyError:
136
+ return _rpc_error(mid, -32602, f"Unknown tool: {name}")
137
+ except Exception as e: # noqa: BLE001 — surface as an MCP tool error, not a 500
138
+ out = {"error": str(e)}
139
+ is_error = isinstance(out, dict) and "error" in out
140
+ return result({
141
+ "content": [{"type": "text", "text": json.dumps(out, default=str, ensure_ascii=False)}],
142
+ "isError": is_error,
143
+ })
144
+ if is_notification:
145
+ return None
146
+ return _rpc_error(mid, -32601, f"Method not found: {method}")
147
+
148
+
149
+ @router.post("/mcp")
150
+ async def mcp_endpoint(request: Request):
151
+ user = _require_caller(request)
152
+ try:
153
+ body = await request.json()
154
+ except Exception:
155
+ return JSONResponse(_rpc_error(None, -32700, "Parse error"), status_code=400)
156
+
157
+ # JSON-RPC batch (a list) or a single message.
158
+ if isinstance(body, list):
159
+ responses = [r for r in (_handle(m, user) for m in body) if r is not None]
160
+ return JSONResponse(responses) if responses else Response(status_code=202)
161
+ resp = _handle(body, user)
162
+ if resp is None:
163
+ return Response(status_code=202)
164
+ return JSONResponse(resp)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.6.11"
3
+ version = "0.6.13"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes