lambda-erp 0.6.10__tar.gz → 0.6.12__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.10 → lambda_erp-0.6.12}/PKG-INFO +1 -1
  2. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/main.py +2 -1
  3. lambda_erp-0.6.12/api/routers/mcp.py +164 -0
  4. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/pyproject.toml +1 -1
  5. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/.gitignore +0 -0
  6. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/LICENSE +0 -0
  7. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/README.md +0 -0
  8. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/__init__.py +0 -0
  9. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/attachments.py +0 -0
  10. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/auth.py +0 -0
  11. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/bootstrap.py +0 -0
  12. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/chat.py +0 -0
  13. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/demo_limits.py +0 -0
  14. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/deps.py +0 -0
  15. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/errors.py +0 -0
  16. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/oauth.py +0 -0
  17. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/pdf.py +0 -0
  18. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/providers.py +0 -0
  19. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/remarks_md.py +0 -0
  20. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/admin.py +0 -0
  23. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/analytics.py +0 -0
  24. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/bank_reconciliation.py +0 -0
  25. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/chat_api.py +0 -0
  26. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/documents.py +0 -0
  27. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/masters.py +0 -0
  28. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/proposals.py +0 -0
  29. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/reports.py +0 -0
  30. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/routers/setup.py +0 -0
  31. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/services.py +0 -0
  32. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/templates/document.html +0 -0
  33. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/api/templates/proposal.html +0 -0
  34. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/docs/agents/README.md +0 -0
  35. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/frontend/LICENSE +0 -0
  36. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/frontend/README.md +0 -0
  37. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/frontend/src/api/client.ts +0 -0
  38. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/__init__.py +0 -0
  39. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/__init__.py +0 -0
  40. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/bank_transaction.py +0 -0
  41. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/budget.py +0 -0
  42. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  43. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/journal_entry.py +0 -0
  45. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/payment_entry.py +0 -0
  46. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/pos_invoice.py +0 -0
  47. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/purchase_invoice.py +0 -0
  48. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/revaluation.py +0 -0
  49. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/sales_invoice.py +0 -0
  50. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/__init__.py +0 -0
  51. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/engine.py +0 -0
  52. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/pack.py +0 -0
  53. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  54. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/packs/ch.py +0 -0
  55. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
  56. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
  57. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
  58. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  59. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/profiles.py +0 -0
  60. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/setup/spine.py +0 -0
  61. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/accounting/subscription.py +0 -0
  62. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/buying/__init__.py +0 -0
  63. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/buying/purchase_order.py +0 -0
  64. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/controllers/__init__.py +0 -0
  65. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/controllers/currency.py +0 -0
  66. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/controllers/defaults.py +0 -0
  67. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/controllers/pricing_rule.py +0 -0
  68. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  69. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/database.py +0 -0
  70. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/exceptions.py +0 -0
  71. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/hooks.py +0 -0
  72. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/model.py +0 -0
  73. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/selling/__init__.py +0 -0
  74. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/selling/proposal.py +0 -0
  75. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/selling/quotation.py +0 -0
  76. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/selling/sales_order.py +0 -0
  77. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/simulation.py +0 -0
  78. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/stock/__init__.py +0 -0
  79. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/stock/delivery_note.py +0 -0
  80. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/stock/purchase_receipt.py +0 -0
  81. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/stock/stock_entry.py +0 -0
  82. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/stock/stock_ledger.py +0 -0
  83. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/lambda_erp/utils.py +0 -0
  84. {lambda_erp-0.6.10 → lambda_erp-0.6.12}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.6.10
3
+ Version: 0.6.12
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: Apache-2.0
@@ -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.10"
3
+ version = "0.6.12"
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
File without changes