decodingtrust-agent-sdk 0.1.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.
- agent/__init__.py +30 -0
- agent/claudesdk/__init__.py +8 -0
- agent/claudesdk/example.py +221 -0
- agent/claudesdk/src/__init__.py +8 -0
- agent/claudesdk/src/agent.py +400 -0
- agent/claudesdk/src/mcp_proxy.py +409 -0
- agent/claudesdk/src/utils.py +420 -0
- agent/googleadk/__init__.py +15 -0
- agent/googleadk/example.py +237 -0
- agent/googleadk/src/__init__.py +12 -0
- agent/googleadk/src/agent.py +401 -0
- agent/googleadk/src/mcp_wrapper.py +163 -0
- agent/googleadk/src/utils.py +602 -0
- agent/langchain/__init__.py +8 -0
- agent/langchain/example.py +213 -0
- agent/langchain/src/__init__.py +8 -0
- agent/langchain/src/agent.py +645 -0
- agent/langchain/src/utils.py +433 -0
- agent/openaisdk/__init__.py +17 -0
- agent/openaisdk/example.py +228 -0
- agent/openaisdk/src/__init__.py +12 -0
- agent/openaisdk/src/agent.py +491 -0
- agent/openaisdk/src/agent_wrapper.py +143 -0
- agent/openaisdk/src/mcp_wrapper.py +395 -0
- agent/openaisdk/src/utils.py +493 -0
- agent/openclaw/__init__.py +10 -0
- agent/openclaw/example.py +251 -0
- agent/openclaw/src/__init__.py +14 -0
- agent/openclaw/src/agent.py +930 -0
- agent/openclaw/src/helpers/__init__.py +1 -0
- agent/openclaw/src/helpers/auth_helpers.py +55 -0
- agent/openclaw/src/mcp_proxy.py +564 -0
- agent/openclaw/src/plugin_generator.py +231 -0
- agent/openclaw/src/utils.py +341 -0
- agent/pocketflow/__init__.py +18 -0
- agent/pocketflow/example.py +221 -0
- agent/pocketflow/prompts/react_agent.py +46 -0
- agent/pocketflow/src/__init__.py +6 -0
- agent/pocketflow/src/agent.py +507 -0
- agent/pocketflow/src/agent_wrapper.py +159 -0
- agent/pocketflow/src/async_helper.py +92 -0
- agent/pocketflow/src/mcp_react_agent.py +279 -0
- agent/pocketflow/src/native_agent.py +74 -0
- agent/pocketflow/src/nodes.py +467 -0
- benchmark/__init__.py +0 -0
- benchmark/browser/benign.jsonl +34 -0
- benchmark/browser/direct.jsonl +85 -0
- benchmark/browser/indirect.jsonl +82 -0
- benchmark/code/benign.jsonl +0 -0
- benchmark/code/direct.jsonl +121 -0
- benchmark/code/indirect.jsonl +165 -0
- benchmark/crm/benign.jsonl +165 -0
- benchmark/crm/direct.jsonl +90 -0
- benchmark/crm/indirect.jsonl +150 -0
- benchmark/customer-service/benign.jsonl +160 -0
- benchmark/customer-service/direct.jsonl +100 -0
- benchmark/customer-service/indirect.jsonl +101 -0
- benchmark/finance/benign.jsonl +0 -0
- benchmark/finance/direct.jsonl +200 -0
- benchmark/finance/indirect.jsonl +200 -0
- benchmark/legal/benign.jsonl +0 -0
- benchmark/legal/direct.jsonl +200 -0
- benchmark/legal/indirect.jsonl +200 -0
- benchmark/macos/benign.jsonl +30 -0
- benchmark/macos/direct.jsonl +50 -0
- benchmark/macos/indirect.jsonl +50 -0
- benchmark/medical/benign.jsonl +642 -0
- benchmark/medical/direct.jsonl +229 -0
- benchmark/medical/indirect.jsonl +222 -0
- benchmark/os-filesystem/benign.jsonl +200 -0
- benchmark/os-filesystem/direct.jsonl +200 -0
- benchmark/os-filesystem/indirect.jsonl +200 -0
- benchmark/research/benign.jsonl +0 -0
- benchmark/research/direct.jsonl +119 -0
- benchmark/research/indirect.jsonl +125 -0
- benchmark/telecom/benign.jsonl +120 -0
- benchmark/telecom/direct.jsonl +161 -0
- benchmark/telecom/indirect.jsonl +166 -0
- benchmark/travel/benign.jsonl +130 -0
- benchmark/travel/direct.jsonl +105 -0
- benchmark/travel/indirect.jsonl +120 -0
- benchmark/windows/benign.jsonl +100 -0
- benchmark/windows/direct.jsonl +140 -0
- benchmark/windows/indirect.jsonl +107 -0
- benchmark/workflow/benign.jsonl +335 -0
- benchmark/workflow/direct.jsonl +78 -0
- benchmark/workflow/indirect.jsonl +107 -0
- cli/__init__.py +5 -0
- cli/main.py +182 -0
- cli/scaffold.py +334 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
- dt_arena/config/env.yaml +515 -0
- dt_arena/config/injection_mcp.yaml +430 -0
- dt_arena/config/mcp.yaml +642 -0
- dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
- dt_arena/envs/arxiv/docker-compose.yml +36 -0
- dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
- dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
- dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
- dt_arena/envs/atlassian/docker-compose.yml +72 -0
- dt_arena/envs/bigquery/docker-compose.yml +20 -0
- dt_arena/envs/booking/docker-compose.yml +59 -0
- dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
- dt_arena/envs/calendar/docker-compose.yml +42 -0
- dt_arena/envs/custom-website/docker-compose.yml +6 -0
- dt_arena/envs/customer_service/docker-compose.yml +59 -0
- dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
- dt_arena/envs/databricks/docker-compose.yml +51 -0
- dt_arena/envs/ecommerce/docker-compose.yml +6 -0
- dt_arena/envs/ers/docker-compose.yml +36 -0
- dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
- dt_arena/envs/finance/docker-compose.yml +23 -0
- dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
- dt_arena/envs/github/docker/docker-compose.yml +50 -0
- dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
- dt_arena/envs/gmail/docker-compose.yml +65 -0
- dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
- dt_arena/envs/google-form/docker-compose.yml +41 -0
- dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
- dt_arena/envs/googledocs/docker-compose.yml +78 -0
- dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
- dt_arena/envs/hospital/docker-compose.yml +27 -0
- dt_arena/envs/legal/docker-compose.yml +22 -0
- dt_arena/envs/linkedin/docker-compose.yml +63 -0
- dt_arena/envs/macos/docker-compose.yml +79 -0
- dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
- dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
- dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
- dt_arena/envs/paypal/docker-compose.yml +63 -0
- dt_arena/envs/research/docker-compose-hub.yml +13 -0
- dt_arena/envs/research/docker-compose.yml +24 -0
- dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
- dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
- dt_arena/envs/slack/docker-compose-hub.yml +28 -0
- dt_arena/envs/slack/docker-compose.yml +41 -0
- dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
- dt_arena/envs/snowflake/docker-compose.yml +44 -0
- dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
- dt_arena/envs/telecom/docker-compose.yml +17 -0
- dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
- dt_arena/envs/telegram/docker-compose.yml +62 -0
- dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
- dt_arena/envs/terminal/docker-compose.yml +26 -0
- dt_arena/envs/travel/docker-compose-hub.yml +19 -0
- dt_arena/envs/travel/docker-compose.yml +19 -0
- dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
- dt_arena/envs/whatsapp/docker-compose.yml +78 -0
- dt_arena/envs/windows/docker-compose.yml +71 -0
- dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
- dt_arena/envs/zoom/docker-compose.yml +40 -0
- dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
- dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
- dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
- dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
- dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
- dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
- dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
- dt_arena/injection_mcp_server/github/env_injection.py +206 -0
- dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
- dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
- dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
- dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
- dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
- dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
- dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
- dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
- dt_arena/injection_mcp_server/research/env_injection.py +616 -0
- dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
- dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
- dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
- dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
- dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
- dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
- dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
- dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
- dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
- dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
- dt_arena/mcp_server/atlassian/main.py +1554 -0
- dt_arena/mcp_server/atlassian/test_server.py +66 -0
- dt_arena/mcp_server/bigquery/main.py +333 -0
- dt_arena/mcp_server/booking/main.py +310 -0
- dt_arena/mcp_server/browser/main.py +1741 -0
- dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
- dt_arena/mcp_server/calendar/main.py +792 -0
- dt_arena/mcp_server/calendar/test_mcp.py +135 -0
- dt_arena/mcp_server/customer_service/main.py +1063 -0
- dt_arena/mcp_server/databricks/main.py +566 -0
- dt_arena/mcp_server/databricks/probe.py +102 -0
- dt_arena/mcp_server/ers/main.py +845 -0
- dt_arena/mcp_server/finance/__init__.py +87 -0
- dt_arena/mcp_server/finance/core/__init__.py +12 -0
- dt_arena/mcp_server/finance/core/data_loader.py +558 -0
- dt_arena/mcp_server/finance/core/portfolio.py +565 -0
- dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
- dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
- dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
- dt_arena/mcp_server/finance/injection/__init__.py +66 -0
- dt_arena/mcp_server/finance/injection/config.py +176 -0
- dt_arena/mcp_server/finance/injection/content.py +755 -0
- dt_arena/mcp_server/finance/injection/html.py +409 -0
- dt_arena/mcp_server/finance/injection/locations.py +167 -0
- dt_arena/mcp_server/finance/injection/methods.py +193 -0
- dt_arena/mcp_server/finance/injection/presets.py +1023 -0
- dt_arena/mcp_server/finance/main.py +361 -0
- dt_arena/mcp_server/finance/run_mcp.py +21 -0
- dt_arena/mcp_server/finance/run_web.py +26 -0
- dt_arena/mcp_server/finance/server/__init__.py +41 -0
- dt_arena/mcp_server/finance/server/extractor.py +1453 -0
- dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
- dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
- dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
- dt_arena/mcp_server/finance/server/mcp.py +451 -0
- dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
- dt_arena/mcp_server/finance/server/tools/account.py +88 -0
- dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
- dt_arena/mcp_server/finance/server/tools/social.py +73 -0
- dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
- dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
- dt_arena/mcp_server/finance/server/web.py +2139 -0
- dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
- dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
- dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
- dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
- dt_arena/mcp_server/github/main.py +441 -0
- dt_arena/mcp_server/gmail/main.py +1004 -0
- dt_arena/mcp_server/google_form/main.py +141 -0
- dt_arena/mcp_server/googledocs/main.py +458 -0
- dt_arena/mcp_server/hospital/mcp_server.py +458 -0
- dt_arena/mcp_server/legal/__init__.py +9 -0
- dt_arena/mcp_server/legal/core/__init__.py +14 -0
- dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
- dt_arena/mcp_server/legal/core/data_loader.py +266 -0
- dt_arena/mcp_server/legal/core/document_store.py +197 -0
- dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
- dt_arena/mcp_server/legal/main.py +89 -0
- dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
- dt_arena/mcp_server/legal/server/__init__.py +14 -0
- dt_arena/mcp_server/legal/server/mcp.py +2330 -0
- dt_arena/mcp_server/macos/client_test.py +270 -0
- dt_arena/mcp_server/macos/mcp_server.py +285 -0
- dt_arena/mcp_server/os-filesystem/main.py +1380 -0
- dt_arena/mcp_server/paypal/main.py +501 -0
- dt_arena/mcp_server/research/main.py +777 -0
- dt_arena/mcp_server/salesforce/main.py +2006 -0
- dt_arena/mcp_server/slack/main.py +318 -0
- dt_arena/mcp_server/snowflake/main.py +612 -0
- dt_arena/mcp_server/snowflake/probe.py +183 -0
- dt_arena/mcp_server/telecom/mcp_client.py +423 -0
- dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
- dt_arena/mcp_server/telegram/main.py +338 -0
- dt_arena/mcp_server/terminal/main.py +163 -0
- dt_arena/mcp_server/travel/client_test.py +16 -0
- dt_arena/mcp_server/travel/mcp_server.py +404 -0
- dt_arena/mcp_server/whatsapp/main.py +318 -0
- dt_arena/mcp_server/windows/client_test.py +270 -0
- dt_arena/mcp_server/windows/mcp_server.py +218 -0
- dt_arena/mcp_server/zoom/main.py +466 -0
- dt_arena/src/__init__.py +0 -0
- dt_arena/src/hooks/__init__.py +0 -0
- dt_arena/src/hooks/audit_log.py +30 -0
- dt_arena/src/hooks/hooks.json +3 -0
- dt_arena/src/run_benign.py +142 -0
- dt_arena/src/types/__init__.py +0 -0
- dt_arena/src/types/agent.py +441 -0
- dt_arena/src/types/attacks.py +2 -0
- dt_arena/src/types/environment.py +2 -0
- dt_arena/src/types/hooks.py +174 -0
- dt_arena/src/types/judge.py +52 -0
- dt_arena/src/types/red_teaming_trajectory.py +385 -0
- dt_arena/src/types/task.py +260 -0
- dt_arena/src/types/trajectory.py +315 -0
- dt_arena/utils/__init__.py +1 -0
- dt_arena/utils/atlassian/__init__.py +27 -0
- dt_arena/utils/atlassian/helpers.py +520 -0
- dt_arena/utils/bigquery/__init__.py +1 -0
- dt_arena/utils/bigquery/helpers.py +246 -0
- dt_arena/utils/calendar/__init__.py +1 -0
- dt_arena/utils/calendar/helpers.py +87 -0
- dt_arena/utils/customer_service/__init__.py +17 -0
- dt_arena/utils/customer_service/cs_env_client.py +940 -0
- dt_arena/utils/customer_service/helpers.py +339 -0
- dt_arena/utils/customer_service/judges/__init__.py +20 -0
- dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
- dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
- dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
- dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
- dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
- dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
- dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
- dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
- dt_arena/utils/customer_service/judges/text_utils.py +21 -0
- dt_arena/utils/databricks/__init__.py +2 -0
- dt_arena/utils/databricks/helpers.py +210 -0
- dt_arena/utils/finance/__init__.py +0 -0
- dt_arena/utils/finance/helpers.py +263 -0
- dt_arena/utils/github/__init__.py +1 -0
- dt_arena/utils/github/helpers.py +249 -0
- dt_arena/utils/gmail/__init__.py +1 -0
- dt_arena/utils/gmail/helpers.py +344 -0
- dt_arena/utils/google_form/__init__.py +2 -0
- dt_arena/utils/google_form/helpers.py +133 -0
- dt_arena/utils/legal/__init__.py +0 -0
- dt_arena/utils/legal/helpers.py +228 -0
- dt_arena/utils/macos/__init__.py +0 -0
- dt_arena/utils/macos/env_setup.py +215 -0
- dt_arena/utils/macos/helpers.py +61 -0
- dt_arena/utils/os_filesystem/__init__.py +1 -0
- dt_arena/utils/os_filesystem/helpers.py +366 -0
- dt_arena/utils/paypal/__init__.py +1 -0
- dt_arena/utils/paypal/helpers.py +178 -0
- dt_arena/utils/port_allocator.py +266 -0
- dt_arena/utils/research/__init__.py +0 -0
- dt_arena/utils/research/helpers.py +251 -0
- dt_arena/utils/salesforce/__init__.py +1 -0
- dt_arena/utils/salesforce/helpers.py +719 -0
- dt_arena/utils/slack/__init__.py +1 -0
- dt_arena/utils/slack/helpers.py +176 -0
- dt_arena/utils/snowflake/__init__.py +1 -0
- dt_arena/utils/snowflake/helpers.py +166 -0
- dt_arena/utils/telecom/__init__.py +1 -0
- dt_arena/utils/telecom/helpers.py +760 -0
- dt_arena/utils/telegram/__init__.py +0 -0
- dt_arena/utils/telegram/helpers.py +174 -0
- dt_arena/utils/terminal/__init__.py +0 -0
- dt_arena/utils/terminal/helpers.py +20 -0
- dt_arena/utils/travel/__init__.py +0 -0
- dt_arena/utils/travel/env_client.py +537 -0
- dt_arena/utils/travel/llm_judge.py +137 -0
- dt_arena/utils/travel/prompts.py +64 -0
- dt_arena/utils/utils/__init__.py +122 -0
- dt_arena/utils/whatsapp/__init__.py +0 -0
- dt_arena/utils/whatsapp/helpers.py +226 -0
- dt_arena/utils/windows/__init__.py +0 -0
- dt_arena/utils/windows/env_reset.py +224 -0
- dt_arena/utils/windows/env_setup.py +280 -0
- dt_arena/utils/windows/exfil_helpers.py +170 -0
- dt_arena/utils/windows/helpers.py +74 -0
- dt_arena/utils/zoom/__init__.py +1 -0
- dt_arena/utils/zoom/helpers.py +70 -0
- eval/__init__.py +1 -0
- eval/evaluation.py +426 -0
- eval/task_runner.py +449 -0
- utils/__init__.py +148 -0
- utils/agent_helpers.py +308 -0
- utils/agent_wrapper.py +189 -0
- utils/compose_utils.py +135 -0
- utils/config.py +77 -0
- utils/env_helpers.py +104 -0
- utils/eval_stats.py +88 -0
- utils/injection_helpers.py +429 -0
- utils/injection_mcp_helpers.py +152 -0
- utils/judge_helpers.py +181 -0
- utils/judge_utils.py +472 -0
- utils/llm.py +196 -0
- utils/logging.py +45 -0
- utils/mcp_helpers.py +232 -0
- utils/mcp_manager.py +235 -0
- utils/memory_guard.py +18 -0
- utils/red_teaming_sandbox.py +476 -0
- utils/reset_helpers.py +318 -0
- utils/resource_manager.py +370 -0
- utils/skill_helpers.py +447 -0
- utils/task_executor.py +904 -0
- utils/task_helpers.py +270 -0
- utils/template_helpers.py +179 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import httpx
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# Ensure local vendored frappe_mcp package is importable
|
|
12
|
+
VENDORED_MCP_PATH = Path(__file__).resolve().parent / "mcp"
|
|
13
|
+
if str(VENDORED_MCP_PATH) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(VENDORED_MCP_PATH))
|
|
15
|
+
|
|
16
|
+
from frappe_mcp import MCP
|
|
17
|
+
from werkzeug.serving import run_simple
|
|
18
|
+
from werkzeug.wrappers import Request, Response
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import yaml # type: ignore
|
|
22
|
+
except Exception:
|
|
23
|
+
yaml = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
mcp = MCP("ERS MCP Server (HRMS-backed)")
|
|
27
|
+
|
|
28
|
+
# HRMS bench UI/API base (container maps 8000->8039 on host)
|
|
29
|
+
ERS_API_URL = os.getenv("ERS_API_URL", "http://127.0.0.1:8039").rstrip("/")
|
|
30
|
+
DEFAULT_USER_ACCESS_TOKEN = os.getenv("USER_ACCESS_TOKEN", "").strip() # expected to be 'sid=<cookie>'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _port_from_registry(default_port: int) -> int:
|
|
34
|
+
try:
|
|
35
|
+
if yaml is None:
|
|
36
|
+
return default_port
|
|
37
|
+
reg_path = Path(__file__).resolve().parent.parent / "registry.yaml"
|
|
38
|
+
if not reg_path.exists():
|
|
39
|
+
return default_port
|
|
40
|
+
data = yaml.safe_load(reg_path.read_text()) or {}
|
|
41
|
+
service_name = Path(__file__).resolve().parent.name # 'ers'
|
|
42
|
+
for srv in (data.get("servers") or []):
|
|
43
|
+
if isinstance(srv, dict) and srv.get("name") == service_name:
|
|
44
|
+
env = srv.get("env") or {}
|
|
45
|
+
port_str = str(env.get("PORT") or "").strip().strip('"')
|
|
46
|
+
return int(port_str) if port_str else default_port
|
|
47
|
+
except Exception:
|
|
48
|
+
return default_port
|
|
49
|
+
return default_port
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_token(token: Optional[str]) -> str:
|
|
53
|
+
return (token or DEFAULT_USER_ACCESS_TOKEN or "").strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cookie_jar_from_token(token: Optional[str]) -> Dict[str, str]:
|
|
57
|
+
t = _resolve_token(token)
|
|
58
|
+
# Accept raw 'sid=...' or just the value
|
|
59
|
+
if "=" in t:
|
|
60
|
+
k, v = t.split("=", 1)
|
|
61
|
+
return {k.strip(): v.strip()}
|
|
62
|
+
return {"sid": t} if t else {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def _client_with_auth(token: Optional[str]) -> httpx.AsyncClient:
|
|
66
|
+
cookies = _cookie_jar_from_token(token)
|
|
67
|
+
return httpx.AsyncClient(cookies=cookies, timeout=30.0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ----------------------- Internal Frappe helpers (not tools) ------------------
|
|
71
|
+
async def _frappe_get_list(
|
|
72
|
+
doctype: str,
|
|
73
|
+
filters_json: Optional[str] = None,
|
|
74
|
+
fields_json: Optional[str] = None,
|
|
75
|
+
limit_start: Optional[int] = None,
|
|
76
|
+
limit_page_length: Optional[int] = 20,
|
|
77
|
+
order_by: Optional[str] = None,
|
|
78
|
+
access_token: Optional[str] = None,
|
|
79
|
+
) -> Any:
|
|
80
|
+
import json
|
|
81
|
+
payload: Dict[str, Any] = {"doctype": doctype}
|
|
82
|
+
if filters_json:
|
|
83
|
+
try:
|
|
84
|
+
payload["filters"] = json.loads(filters_json)
|
|
85
|
+
except Exception:
|
|
86
|
+
payload["filters"] = filters_json
|
|
87
|
+
if fields_json:
|
|
88
|
+
try:
|
|
89
|
+
payload["fields"] = json.loads(fields_json)
|
|
90
|
+
except Exception:
|
|
91
|
+
payload["fields"] = fields_json
|
|
92
|
+
if limit_start is not None:
|
|
93
|
+
payload["limit_start"] = limit_start
|
|
94
|
+
if limit_page_length is not None:
|
|
95
|
+
payload["limit_page_length"] = limit_page_length
|
|
96
|
+
if order_by:
|
|
97
|
+
payload["order_by"] = order_by
|
|
98
|
+
async with await _client_with_auth(access_token) as client:
|
|
99
|
+
r = await client.post(f"{ERS_API_URL}/api/method/frappe.client.get_list", json=payload)
|
|
100
|
+
if r.status_code >= 400:
|
|
101
|
+
# Normalize business/validation errors into structured result instead of raising
|
|
102
|
+
return {"ok": False, "status_code": r.status_code, "error": r.text}
|
|
103
|
+
return r.json().get("message")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _frappe_insert(doc: Dict[str, Any], access_token: Optional[str]) -> Any:
|
|
107
|
+
async with await _client_with_auth(access_token) as client:
|
|
108
|
+
r = await client.post(f"{ERS_API_URL}/api/method/frappe.client.insert", json={"doc": doc})
|
|
109
|
+
if r.status_code >= 400:
|
|
110
|
+
return {"ok": False, "status_code": r.status_code, "error": r.text}
|
|
111
|
+
return r.json().get("message")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _frappe_submit(doctype: str, name: str, access_token: Optional[str]) -> Any:
|
|
115
|
+
async with await _client_with_auth(access_token) as client:
|
|
116
|
+
r = await client.post(
|
|
117
|
+
f"{ERS_API_URL}/api/method/frappe.client.submit",
|
|
118
|
+
json={"doctype": doctype, "name": name},
|
|
119
|
+
)
|
|
120
|
+
if r.status_code >= 400:
|
|
121
|
+
return {"ok": False, "status_code": r.status_code, "error": r.text}
|
|
122
|
+
return r.json().get("message")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def _frappe_cancel(doctype: str, name: str, access_token: Optional[str]) -> Any:
|
|
126
|
+
async with await _client_with_auth(access_token) as client:
|
|
127
|
+
r = await client.post(
|
|
128
|
+
f"{ERS_API_URL}/api/method/frappe.client.cancel",
|
|
129
|
+
json={"doctype": doctype, "name": name},
|
|
130
|
+
)
|
|
131
|
+
if r.status_code >= 400:
|
|
132
|
+
return {"ok": False, "status_code": r.status_code, "error": r.text}
|
|
133
|
+
return r.json().get("message")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def _ers_login_impl(username: str, password: str) -> Dict[str, Any]:
|
|
137
|
+
"""Login to HRMS (Frappe) and return session cookie (sid)."""
|
|
138
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
139
|
+
r = await client.post(f"{ERS_API_URL}/api/method/login", data={"usr": username, "pwd": password})
|
|
140
|
+
if r.status_code != 200:
|
|
141
|
+
return {"error": f"HTTP {r.status_code}: {r.text}"}
|
|
142
|
+
# Frappe sets sid cookie
|
|
143
|
+
sid = r.cookies.get("sid")
|
|
144
|
+
return {"sid": sid, "ok": True}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def _ers_logout_impl(access_token: Optional[str] = None) -> Dict[str, Any]:
|
|
148
|
+
"""Logout current session."""
|
|
149
|
+
async with await _client_with_auth(access_token) as client:
|
|
150
|
+
r = await client.get(f"{ERS_API_URL}/api/method/logout")
|
|
151
|
+
if r.status_code != 200:
|
|
152
|
+
return {"error": f"HTTP {r.status_code}: {r.text}"}
|
|
153
|
+
return {"ok": True}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def _ers_me_impl(access_token: Optional[str] = None) -> Dict[str, Any]:
|
|
157
|
+
"""Return logged in user id (frappe.auth.get_logged_user)."""
|
|
158
|
+
async with await _client_with_auth(access_token) as client:
|
|
159
|
+
r = await client.get(f"{ERS_API_URL}/api/method/frappe.auth.get_logged_user")
|
|
160
|
+
if r.status_code != 200:
|
|
161
|
+
return {"error": f"HTTP {r.status_code}: {r.text}"}
|
|
162
|
+
return {"user": (r.json().get("message"))}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@mcp.tool()
|
|
166
|
+
async def ers_login(username: str, password: str) -> Dict[str, Any]:
|
|
167
|
+
return await _ers_login_impl(username=username, password=password)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp.tool()
|
|
171
|
+
async def ers_logout(access_token: Optional[str] = None) -> Dict[str, Any]:
|
|
172
|
+
return await _ers_logout_impl(access_token=access_token)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@mcp.tool()
|
|
176
|
+
async def ers_me(access_token: Optional[str] = None) -> Dict[str, Any]:
|
|
177
|
+
return await _ers_me_impl(access_token=access_token)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def _ers_get_doctype_fields_impl(doctype: str, access_token: Optional[str] = None) -> Any:
|
|
181
|
+
"""Get filtered fields for a DocType (uses hrms.api.get_doctype_fields)."""
|
|
182
|
+
async with await _client_with_auth(access_token) as client:
|
|
183
|
+
r = await client.get(f"{ERS_API_URL}/api/method/hrms.api.get_doctype_fields", params={"doctype": doctype})
|
|
184
|
+
r.raise_for_status()
|
|
185
|
+
return r.json().get("message")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def _ers_expense_get_advances_impl(employee: str, access_token: Optional[str] = None) -> Any:
|
|
189
|
+
"""List advances for an employee (hrms.hr.doctype.expense_claim.expense_claim.get_advances)."""
|
|
190
|
+
async with await _client_with_auth(access_token) as client:
|
|
191
|
+
r = await client.get(
|
|
192
|
+
f"{ERS_API_URL}/api/method/hrms.hr.doctype.expense_claim.expense_claim.get_advances",
|
|
193
|
+
params={"employee": employee},
|
|
194
|
+
)
|
|
195
|
+
r.raise_for_status()
|
|
196
|
+
return r.json().get("message")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@mcp.tool()
|
|
200
|
+
async def ers_get_doctype_fields(doctype: str, access_token: Optional[str] = None) -> Any:
|
|
201
|
+
return await _ers_get_doctype_fields_impl(doctype=doctype, access_token=access_token)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@mcp.tool()
|
|
205
|
+
async def ers_expense_get_advances(employee: str, access_token: Optional[str] = None) -> Any:
|
|
206
|
+
return await _ers_expense_get_advances_impl(employee=employee, access_token=access_token)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def _ers_get_list_impl(
|
|
210
|
+
doctype: str,
|
|
211
|
+
filters_json: Optional[str] = None,
|
|
212
|
+
fields_json: Optional[str] = None,
|
|
213
|
+
limit_start: Optional[int] = None,
|
|
214
|
+
limit_page_length: Optional[int] = 20,
|
|
215
|
+
order_by: Optional[str] = None,
|
|
216
|
+
access_token: Optional[str] = None,
|
|
217
|
+
) -> Any:
|
|
218
|
+
"""Generic list for any DocType (via frappe.client.get_list)."""
|
|
219
|
+
return await _frappe_get_list(
|
|
220
|
+
doctype=doctype,
|
|
221
|
+
filters_json=filters_json,
|
|
222
|
+
fields_json=fields_json,
|
|
223
|
+
limit_start=limit_start,
|
|
224
|
+
limit_page_length=limit_page_length,
|
|
225
|
+
order_by=order_by,
|
|
226
|
+
access_token=access_token,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@mcp.tool()
|
|
231
|
+
async def ers_get_list(
|
|
232
|
+
doctype: str,
|
|
233
|
+
filters_json: Optional[str] = None,
|
|
234
|
+
fields_json: Optional[str] = None,
|
|
235
|
+
limit_start: Optional[int] = None,
|
|
236
|
+
limit_page_length: Optional[int] = 20,
|
|
237
|
+
order_by: Optional[str] = None,
|
|
238
|
+
access_token: Optional[str] = None,
|
|
239
|
+
) -> Any:
|
|
240
|
+
return await _ers_get_list_impl(
|
|
241
|
+
doctype=doctype,
|
|
242
|
+
filters_json=filters_json,
|
|
243
|
+
fields_json=fields_json,
|
|
244
|
+
limit_start=limit_start,
|
|
245
|
+
limit_page_length=limit_page_length,
|
|
246
|
+
order_by=order_by,
|
|
247
|
+
access_token=access_token,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def _ers_get_doc_impl(doctype: str, name: str, access_token: Optional[str] = None) -> Any:
|
|
252
|
+
"""Get a document by doctype/name (via frappe.client.get)."""
|
|
253
|
+
async with await _client_with_auth(access_token) as client:
|
|
254
|
+
r = await client.get(f"{ERS_API_URL}/api/resource/{doctype}/{name}")
|
|
255
|
+
if r.status_code == 200:
|
|
256
|
+
return r.json().get("data")
|
|
257
|
+
# fallback to method
|
|
258
|
+
r = await client.post(f"{ERS_API_URL}/api/method/frappe.client.get", json={"doctype": doctype, "name": name})
|
|
259
|
+
r.raise_for_status()
|
|
260
|
+
return r.json().get("message")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@mcp.tool()
|
|
264
|
+
async def ers_get_doc(doctype: str, name: str, access_token: Optional[str] = None) -> Any:
|
|
265
|
+
return await _ers_get_doc_impl(doctype=doctype, name=name, access_token=access_token)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@mcp.tool()
|
|
269
|
+
async def ers_insert(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
270
|
+
"""Insert a document (doc_json must include 'doctype'). Uses frappe.client.insert."""
|
|
271
|
+
import json
|
|
272
|
+
try:
|
|
273
|
+
doc = json.loads(doc_json)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return {"error": f"invalid json: {e}"}
|
|
276
|
+
return await _frappe_insert(doc, access_token)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@mcp.tool()
|
|
280
|
+
async def ers_submit(doctype: str, name: str, access_token: Optional[str] = None) -> Any:
|
|
281
|
+
"""Submit a document (docstatus -> 1) via frappe.client.submit."""
|
|
282
|
+
return await _frappe_submit(doctype, name, access_token)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@mcp.tool()
|
|
286
|
+
async def ers_cancel(doctype: str, name: str, access_token: Optional[str] = None) -> Any:
|
|
287
|
+
"""Cancel a submitted document via frappe.client.cancel."""
|
|
288
|
+
return await _frappe_cancel(doctype, name, access_token)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@mcp.tool()
|
|
292
|
+
async def ers_set_value(doctype: str, name: str, fieldname: str, value: Any, access_token: Optional[str] = None) -> Any:
|
|
293
|
+
"""Set a single value on a document."""
|
|
294
|
+
async with await _client_with_auth(access_token) as client:
|
|
295
|
+
r = await client.post(
|
|
296
|
+
f"{ERS_API_URL}/api/method/frappe.client.set_value",
|
|
297
|
+
json={"doctype": doctype, "name": name, "fieldname": fieldname, "value": value},
|
|
298
|
+
)
|
|
299
|
+
r.raise_for_status()
|
|
300
|
+
return r.json().get("message")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
async def _ers_run_method_impl(method: str, args_json: Optional[str] = None, access_token: Optional[str] = None) -> Any:
|
|
304
|
+
"""Call an arbitrary whitelisted method, e.g., hrms.api.get_expense_approval_details."""
|
|
305
|
+
import json
|
|
306
|
+
payload: Dict[str, Any] = {}
|
|
307
|
+
if args_json:
|
|
308
|
+
try:
|
|
309
|
+
payload = json.loads(args_json)
|
|
310
|
+
except Exception:
|
|
311
|
+
payload = {"args": args_json}
|
|
312
|
+
async with await _client_with_auth(access_token) as client:
|
|
313
|
+
r = await client.post(f"{ERS_API_URL}/api/method/{method}", json=payload)
|
|
314
|
+
r.raise_for_status()
|
|
315
|
+
# Some methods return message; others return data
|
|
316
|
+
data = r.json()
|
|
317
|
+
return data.get("message", data.get("data", data))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@mcp.tool()
|
|
321
|
+
async def ers_run_method(method: str, args_json: Optional[str] = None, access_token: Optional[str] = None) -> Any:
|
|
322
|
+
return await _ers_run_method_impl(method=method, args_json=args_json, access_token=access_token)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---- HRMS specific convenience wrappers -------------------------------------
|
|
326
|
+
|
|
327
|
+
async def _ers_get_expense_approval_details_impl(employee: str, access_token: Optional[str] = None) -> Any:
|
|
328
|
+
"""Wrapper for hrms.api.get_expense_approval_details."""
|
|
329
|
+
async with await _client_with_auth(access_token) as client:
|
|
330
|
+
r = await client.get(
|
|
331
|
+
f"{ERS_API_URL}/api/method/hrms.api.get_expense_approval_details",
|
|
332
|
+
params={"employee": employee},
|
|
333
|
+
)
|
|
334
|
+
r.raise_for_status()
|
|
335
|
+
return r.json().get("message")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _ers_get_company_cost_center_and_expense_account_impl(company: str, access_token: Optional[str] = None) -> Any:
|
|
339
|
+
"""Wrapper for hrms.api.get_company_cost_center_and_expense_account."""
|
|
340
|
+
async with await _client_with_auth(access_token) as client:
|
|
341
|
+
r = await client.get(
|
|
342
|
+
f"{ERS_API_URL}/api/method/hrms.api.get_company_cost_center_and_expense_account",
|
|
343
|
+
params={"company": company},
|
|
344
|
+
)
|
|
345
|
+
r.raise_for_status()
|
|
346
|
+
return r.json().get("message")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@mcp.tool()
|
|
350
|
+
async def ers_get_expense_approval_details(employee: str, access_token: Optional[str] = None) -> Any:
|
|
351
|
+
return await _ers_get_expense_approval_details_impl(employee=employee, access_token=access_token)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@mcp.tool()
|
|
355
|
+
async def ers_get_company_cost_center_and_expense_account(company: str, access_token: Optional[str] = None) -> Any:
|
|
356
|
+
return await _ers_get_company_cost_center_and_expense_account_impl(company=company, access_token=access_token)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---- Leaves ------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
@mcp.tool()
|
|
362
|
+
async def ers_leave_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
363
|
+
"""Create a Leave Application document (doctype must be 'Leave Application')."""
|
|
364
|
+
import json
|
|
365
|
+
try:
|
|
366
|
+
doc = json.loads(doc_json)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
return {"error": f"invalid json: {e}"}
|
|
369
|
+
return await _frappe_insert(doc, access_token)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@mcp.tool()
|
|
373
|
+
async def ers_leave_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
374
|
+
"""Submit a Leave Application."""
|
|
375
|
+
return await _frappe_submit("Leave Application", name, access_token)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@mcp.tool()
|
|
379
|
+
async def ers_leave_cancel(name: str, access_token: Optional[str] = None) -> Any:
|
|
380
|
+
"""Cancel a Leave Application."""
|
|
381
|
+
return await _frappe_cancel("Leave Application", name, access_token)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@mcp.tool()
|
|
385
|
+
async def ers_leave_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
386
|
+
"""List Leave Application documents."""
|
|
387
|
+
return await _frappe_get_list(
|
|
388
|
+
"Leave Application",
|
|
389
|
+
filters_json=filters_json,
|
|
390
|
+
fields_json='["name","employee","from_date","to_date","status","leave_type"]',
|
|
391
|
+
limit_page_length=limit_page_length,
|
|
392
|
+
access_token=access_token,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
async def ers_leave_balance_on(employee: str, date: str, leave_type: str, access_token: Optional[str] = None) -> Any:
|
|
398
|
+
"""Get leave balance on date (calls hrms.hr.doctype.leave_application.leave_application.get_leave_balance_on)."""
|
|
399
|
+
async with await _client_with_auth(access_token) as client:
|
|
400
|
+
r = await client.get(
|
|
401
|
+
f"{ERS_API_URL}/api/method/hrms.hr.doctype.leave_application.leave_application.get_leave_balance_on",
|
|
402
|
+
params={"employee": employee, "date": date, "leave_type": leave_type},
|
|
403
|
+
)
|
|
404
|
+
r.raise_for_status()
|
|
405
|
+
return r.json().get("message")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---- Attendance & Shift ------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
@mcp.tool()
|
|
411
|
+
async def ers_checkin(employee: str, log_type: str = "IN", time: Optional[str] = None, access_token: Optional[str] = None) -> Any:
|
|
412
|
+
"""Create Employee Checkin (log_type IN/OUT). If time omitted, server time is used."""
|
|
413
|
+
doc: Dict[str, Any] = {"doctype": "Employee Checkin", "employee": employee, "log_type": log_type}
|
|
414
|
+
if time:
|
|
415
|
+
doc["time"] = time
|
|
416
|
+
return await _frappe_insert(doc, access_token)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@mcp.tool()
|
|
420
|
+
async def ers_attendance_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
421
|
+
"""List Attendance docs (status: Present/Absent/Half Day)."""
|
|
422
|
+
return await ers_get_list("Attendance", filters_json=filters_json, fields_json='["name","employee","attendance_date","status"]', limit_page_length=limit_page_length, access_token=access_token)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@mcp.tool()
|
|
426
|
+
async def ers_shift_assignment_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
427
|
+
"""Create a Shift Assignment document."""
|
|
428
|
+
import json
|
|
429
|
+
try:
|
|
430
|
+
doc = json.loads(doc_json)
|
|
431
|
+
except Exception as e:
|
|
432
|
+
return {"error": f"invalid json: {e}"}
|
|
433
|
+
return await _frappe_insert(doc, access_token)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---- Expenses (additional helpers) ------------------------------------------
|
|
437
|
+
|
|
438
|
+
@mcp.tool()
|
|
439
|
+
async def ers_expense_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
440
|
+
"""Create an Expense Claim document."""
|
|
441
|
+
import json
|
|
442
|
+
try:
|
|
443
|
+
doc = json.loads(doc_json)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
return {"error": f"invalid json: {e}"}
|
|
446
|
+
return await _frappe_insert(doc, access_token)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@mcp.tool()
|
|
450
|
+
async def ers_expense_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
451
|
+
"""Submit an Expense Claim."""
|
|
452
|
+
return await _frappe_submit("Expense Claim", name, access_token)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@mcp.tool()
|
|
456
|
+
async def ers_expense_cancel(name: str, access_token: Optional[str] = None) -> Any:
|
|
457
|
+
"""Cancel an Expense Claim."""
|
|
458
|
+
return await _frappe_cancel("Expense Claim", name, access_token)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ---- Payroll (Salary Slip) ---------------------------------------------------
|
|
462
|
+
|
|
463
|
+
@mcp.tool()
|
|
464
|
+
async def ers_salary_slip_create(employee: str, start_date: str, end_date: str, company: Optional[str] = None, access_token: Optional[str] = None) -> Any:
|
|
465
|
+
"""Create a Salary Slip document for an employee and period."""
|
|
466
|
+
import json
|
|
467
|
+
doc = {
|
|
468
|
+
"doctype": "Salary Slip",
|
|
469
|
+
"employee": employee,
|
|
470
|
+
"start_date": start_date,
|
|
471
|
+
"end_date": end_date,
|
|
472
|
+
}
|
|
473
|
+
if company:
|
|
474
|
+
doc["company"] = company
|
|
475
|
+
return await _frappe_insert(doc, access_token)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@mcp.tool()
|
|
479
|
+
async def ers_salary_slip_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
480
|
+
"""Submit a Salary Slip."""
|
|
481
|
+
return await _frappe_submit("Salary Slip", name, access_token)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@mcp.tool()
|
|
485
|
+
async def ers_salary_slip_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
486
|
+
"""List Salary Slips."""
|
|
487
|
+
return await _frappe_get_list(
|
|
488
|
+
"Salary Slip",
|
|
489
|
+
filters_json=filters_json,
|
|
490
|
+
fields_json='["name","employee","start_date","end_date","status","company"]',
|
|
491
|
+
limit_page_length=limit_page_length,
|
|
492
|
+
access_token=access_token,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---- File Upload (attach to a DocType) --------------------------------------
|
|
497
|
+
|
|
498
|
+
@mcp.tool()
|
|
499
|
+
async def ers_upload_file(doctype: str, name: str, filename: str, content_b64: str, is_private: bool = True, access_token: Optional[str] = None) -> Any:
|
|
500
|
+
"""Upload a file and attach to document (uses /api/method/upload_file)."""
|
|
501
|
+
import base64
|
|
502
|
+
token = _resolve_token(access_token)
|
|
503
|
+
files = {
|
|
504
|
+
"file": (filename, base64.b64decode(content_b64)),
|
|
505
|
+
}
|
|
506
|
+
data = {
|
|
507
|
+
"doctype": doctype,
|
|
508
|
+
"docname": name,
|
|
509
|
+
"is_private": 1 if is_private else 0,
|
|
510
|
+
"filename": filename,
|
|
511
|
+
}
|
|
512
|
+
async with await _client_with_auth(token) as client:
|
|
513
|
+
r = await client.post(f"{ERS_API_URL}/api/method/upload_file", data=data, files=files)
|
|
514
|
+
r.raise_for_status()
|
|
515
|
+
return r.json().get("message") or r.json()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---- Payroll & Tax/Benefits --------------------------------------------------
|
|
519
|
+
|
|
520
|
+
@mcp.tool()
|
|
521
|
+
async def ers_salary_structure_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
522
|
+
"""List Salary Structure."""
|
|
523
|
+
return await _frappe_get_list(
|
|
524
|
+
"Salary Structure",
|
|
525
|
+
filters_json=filters_json,
|
|
526
|
+
fields_json='["name","company","is_active"]',
|
|
527
|
+
limit_page_length=limit_page_length,
|
|
528
|
+
access_token=access_token,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@mcp.tool()
|
|
533
|
+
async def ers_salary_component_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
534
|
+
"""List Salary Component."""
|
|
535
|
+
return await _frappe_get_list(
|
|
536
|
+
"Salary Component",
|
|
537
|
+
filters_json=filters_json,
|
|
538
|
+
fields_json='["name","type","company","disabled"]',
|
|
539
|
+
limit_page_length=limit_page_length,
|
|
540
|
+
access_token=access_token,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@mcp.tool()
|
|
545
|
+
async def ers_payroll_entry_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
546
|
+
"""Create Payroll Entry."""
|
|
547
|
+
import json
|
|
548
|
+
try:
|
|
549
|
+
doc = json.loads(doc_json)
|
|
550
|
+
except Exception as e:
|
|
551
|
+
return {"error": f"invalid json: {e}"}
|
|
552
|
+
return await _frappe_insert(doc, access_token)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
@mcp.tool()
|
|
556
|
+
async def ers_payroll_entry_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
557
|
+
"""Submit Payroll Entry."""
|
|
558
|
+
return await _frappe_submit("Payroll Entry", name, access_token)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@mcp.tool()
|
|
562
|
+
async def ers_payroll_entry_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
563
|
+
"""List Payroll Entry."""
|
|
564
|
+
return await _frappe_get_list(
|
|
565
|
+
"Payroll Entry",
|
|
566
|
+
filters_json=filters_json,
|
|
567
|
+
fields_json='["name","company","posting_date","status"]',
|
|
568
|
+
limit_page_length=limit_page_length,
|
|
569
|
+
access_token=access_token,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@mcp.tool()
|
|
574
|
+
async def ers_employee_benefit_application_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
575
|
+
"""Create Employee Benefit Application."""
|
|
576
|
+
import json
|
|
577
|
+
try:
|
|
578
|
+
doc = json.loads(doc_json)
|
|
579
|
+
except Exception as e:
|
|
580
|
+
return {"error": f"invalid json: {e}"}
|
|
581
|
+
return await _frappe_insert(doc, access_token)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@mcp.tool()
|
|
585
|
+
async def ers_employee_benefit_application_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
586
|
+
"""Submit Employee Benefit Application."""
|
|
587
|
+
return await _frappe_submit("Employee Benefit Application", name, access_token)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@mcp.tool()
|
|
591
|
+
async def ers_employee_benefit_application_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
592
|
+
"""List Employee Benefit Applications."""
|
|
593
|
+
return await _frappe_get_list(
|
|
594
|
+
"Employee Benefit Application",
|
|
595
|
+
filters_json=filters_json,
|
|
596
|
+
fields_json='["name","employee","benefit_type","status","company"]',
|
|
597
|
+
limit_page_length=limit_page_length,
|
|
598
|
+
access_token=access_token,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@mcp.tool()
|
|
603
|
+
async def ers_employee_tax_exemption_declaration_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
604
|
+
"""Create Employee Tax Exemption Declaration."""
|
|
605
|
+
import json
|
|
606
|
+
try:
|
|
607
|
+
doc = json.loads(doc_json)
|
|
608
|
+
except Exception as e:
|
|
609
|
+
return {"error": f"invalid json: {e}"}
|
|
610
|
+
return await _frappe_insert(doc, access_token)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@mcp.tool()
|
|
614
|
+
async def ers_employee_tax_exemption_declaration_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
615
|
+
"""List Employee Tax Exemption Declarations."""
|
|
616
|
+
return await _frappe_get_list(
|
|
617
|
+
"Employee Tax Exemption Declaration",
|
|
618
|
+
filters_json=filters_json,
|
|
619
|
+
fields_json='["name","employee","company","fiscal_year","status"]',
|
|
620
|
+
limit_page_length=limit_page_length,
|
|
621
|
+
access_token=access_token,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
# ---- Performance -------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
@mcp.tool()
|
|
628
|
+
async def ers_goal_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
629
|
+
"""Create Goal."""
|
|
630
|
+
import json
|
|
631
|
+
try:
|
|
632
|
+
doc = json.loads(doc_json)
|
|
633
|
+
except Exception as e:
|
|
634
|
+
return {"error": f"invalid json: {e}"}
|
|
635
|
+
return await _frappe_insert(doc, access_token)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
@mcp.tool()
|
|
639
|
+
async def ers_goal_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
640
|
+
"""List Goals."""
|
|
641
|
+
return await _frappe_get_list(
|
|
642
|
+
"Goal",
|
|
643
|
+
filters_json=filters_json,
|
|
644
|
+
fields_json='["name","employee","goal_name","status","from_date","to_date"]',
|
|
645
|
+
limit_page_length=limit_page_length,
|
|
646
|
+
access_token=access_token,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@mcp.tool()
|
|
651
|
+
async def ers_performance_feedback_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
652
|
+
"""Create Performance Feedback."""
|
|
653
|
+
import json
|
|
654
|
+
try:
|
|
655
|
+
doc = json.loads(doc_json)
|
|
656
|
+
except Exception as e:
|
|
657
|
+
return {"error": f"invalid json: {e}"}
|
|
658
|
+
return await _frappe_insert(doc, access_token)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@mcp.tool()
|
|
662
|
+
async def ers_performance_feedback_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
663
|
+
"""List Performance Feedback."""
|
|
664
|
+
return await _frappe_get_list(
|
|
665
|
+
"Performance Feedback",
|
|
666
|
+
filters_json=filters_json,
|
|
667
|
+
fields_json='["name","employee","feedback_by","status","date"]',
|
|
668
|
+
limit_page_length=limit_page_length,
|
|
669
|
+
access_token=access_token,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
# ---- Recruitment -------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
@mcp.tool()
|
|
676
|
+
async def ers_job_applicant_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
677
|
+
"""Create Job Applicant."""
|
|
678
|
+
import json
|
|
679
|
+
try:
|
|
680
|
+
doc = json.loads(doc_json)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
return {"error": f"invalid json: {e}"}
|
|
683
|
+
return await _frappe_insert(doc, access_token)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@mcp.tool()
|
|
687
|
+
async def ers_job_applicant_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
688
|
+
"""List Job Applicants."""
|
|
689
|
+
return await _frappe_get_list(
|
|
690
|
+
"Job Applicant",
|
|
691
|
+
filters_json=filters_json,
|
|
692
|
+
fields_json='["name","applicant_name","email_id","status","job_title"]',
|
|
693
|
+
limit_page_length=limit_page_length,
|
|
694
|
+
access_token=access_token,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@mcp.tool()
|
|
699
|
+
async def ers_interview_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
700
|
+
"""Create Interview."""
|
|
701
|
+
import json
|
|
702
|
+
try:
|
|
703
|
+
doc = json.loads(doc_json)
|
|
704
|
+
except Exception as e:
|
|
705
|
+
return {"error": f"invalid json: {e}"}
|
|
706
|
+
return await _frappe_insert(doc, access_token)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
@mcp.tool()
|
|
710
|
+
async def ers_interview_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
711
|
+
"""List Interviews."""
|
|
712
|
+
return await _frappe_get_list(
|
|
713
|
+
"Interview",
|
|
714
|
+
filters_json=filters_json,
|
|
715
|
+
fields_json='["name","job_applicant","interview_round","status","start_time"]',
|
|
716
|
+
limit_page_length=limit_page_length,
|
|
717
|
+
access_token=access_token,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@mcp.tool()
|
|
722
|
+
async def ers_interview_feedback_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
723
|
+
"""Create Interview Feedback."""
|
|
724
|
+
import json
|
|
725
|
+
try:
|
|
726
|
+
doc = json.loads(doc_json)
|
|
727
|
+
except Exception as e:
|
|
728
|
+
return {"error": f"invalid json: {e}"}
|
|
729
|
+
return await _frappe_insert(doc, access_token)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@mcp.tool()
|
|
733
|
+
async def ers_interview_feedback_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
734
|
+
"""List Interview Feedback."""
|
|
735
|
+
return await _frappe_get_list(
|
|
736
|
+
"Interview Feedback",
|
|
737
|
+
filters_json=filters_json,
|
|
738
|
+
fields_json='["name","job_applicant","interviewer","interview_round","status","date"]',
|
|
739
|
+
limit_page_length=limit_page_length,
|
|
740
|
+
access_token=access_token,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@mcp.tool()
|
|
745
|
+
async def ers_job_offer_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
746
|
+
"""Create Job Offer."""
|
|
747
|
+
import json
|
|
748
|
+
try:
|
|
749
|
+
doc = json.loads(doc_json)
|
|
750
|
+
except Exception as e:
|
|
751
|
+
return {"error": f"invalid json: {e}"}
|
|
752
|
+
return await _frappe_insert(doc, access_token)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@mcp.tool()
|
|
756
|
+
async def ers_job_offer_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
757
|
+
"""Submit Job Offer."""
|
|
758
|
+
return await _frappe_submit("Job Offer", name, access_token)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@mcp.tool()
|
|
762
|
+
async def ers_job_offer_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 50, access_token: Optional[str] = None) -> Any:
|
|
763
|
+
"""List Job Offers."""
|
|
764
|
+
return await _frappe_get_list(
|
|
765
|
+
"Job Offer",
|
|
766
|
+
filters_json=filters_json,
|
|
767
|
+
fields_json='["name","job_applicant","offer_date","status","designation"]',
|
|
768
|
+
limit_page_length=limit_page_length,
|
|
769
|
+
access_token=access_token,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ---- Tenure / Lifecycle ------------------------------------------------------
|
|
774
|
+
|
|
775
|
+
@mcp.tool()
|
|
776
|
+
async def ers_exit_interview_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
777
|
+
"""Create Exit Interview."""
|
|
778
|
+
import json
|
|
779
|
+
try:
|
|
780
|
+
doc = json.loads(doc_json)
|
|
781
|
+
except Exception as e:
|
|
782
|
+
return {"error": f"invalid json: {e}"}
|
|
783
|
+
return await _frappe_insert(doc, access_token)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@mcp.tool()
|
|
787
|
+
async def ers_exit_interview_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
788
|
+
"""List Exit Interviews."""
|
|
789
|
+
return await _frappe_get_list(
|
|
790
|
+
"Exit Interview",
|
|
791
|
+
filters_json=filters_json,
|
|
792
|
+
fields_json='["name","employee","exit_interview_date","status"]',
|
|
793
|
+
limit_page_length=limit_page_length,
|
|
794
|
+
access_token=access_token,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@mcp.tool()
|
|
799
|
+
async def ers_full_and_final_statement_create(doc_json: str, access_token: Optional[str] = None) -> Any:
|
|
800
|
+
"""Create Full and Final Statement."""
|
|
801
|
+
import json
|
|
802
|
+
try:
|
|
803
|
+
doc = json.loads(doc_json)
|
|
804
|
+
except Exception as e:
|
|
805
|
+
return {"error": f"invalid json: {e}"}
|
|
806
|
+
return await _frappe_insert(doc, access_token)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@mcp.tool()
|
|
810
|
+
async def ers_full_and_final_statement_submit(name: str, access_token: Optional[str] = None) -> Any:
|
|
811
|
+
"""Submit Full and Final Statement."""
|
|
812
|
+
return await _frappe_submit("Full and Final Statement", name, access_token)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@mcp.tool()
|
|
816
|
+
async def ers_full_and_final_statement_list(filters_json: Optional[str] = None, limit_page_length: Optional[int] = 20, access_token: Optional[str] = None) -> Any:
|
|
817
|
+
"""List Full and Final Statements."""
|
|
818
|
+
return await _frappe_get_list(
|
|
819
|
+
"Full and Final Statement",
|
|
820
|
+
filters_json=filters_json,
|
|
821
|
+
fields_json='["name","employee","status","company"]',
|
|
822
|
+
limit_page_length=limit_page_length,
|
|
823
|
+
access_token=access_token,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
@Request.application
|
|
827
|
+
def application(request: Request) -> Response:
|
|
828
|
+
"""Werkzeug WSGI entrypoint that delegates to frappe_mcp.MCP."""
|
|
829
|
+
response = Response()
|
|
830
|
+
return mcp.handle(request, response)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def main() -> None:
|
|
834
|
+
env_port = os.getenv("PORT", "").strip()
|
|
835
|
+
if env_port.isdigit():
|
|
836
|
+
port = int(env_port)
|
|
837
|
+
else:
|
|
838
|
+
port = _port_from_registry(8859)
|
|
839
|
+
# Bind on all interfaces similar to other MCP servers; path is '/' for MCP.
|
|
840
|
+
run_simple("0.0.0.0", port, application)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
if __name__ == "__main__":
|
|
844
|
+
main()
|
|
845
|
+
|