agentvee-mcp 0.1.2__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 (30) hide show
  1. agentvee_mcp-0.1.2/.gitignore +4 -0
  2. agentvee_mcp-0.1.2/PKG-INFO +137 -0
  3. agentvee_mcp-0.1.2/README.md +118 -0
  4. agentvee_mcp-0.1.2/agentvee_mcp/__init__.py +3 -0
  5. agentvee_mcp-0.1.2/agentvee_mcp/__main__.py +3 -0
  6. agentvee_mcp-0.1.2/agentvee_mcp/api_client.py +175 -0
  7. agentvee_mcp-0.1.2/agentvee_mcp/auth.py +18 -0
  8. agentvee_mcp-0.1.2/agentvee_mcp/config.py +107 -0
  9. agentvee_mcp-0.1.2/agentvee_mcp/http.py +18 -0
  10. agentvee_mcp-0.1.2/agentvee_mcp/security/__init__.py +0 -0
  11. agentvee_mcp-0.1.2/agentvee_mcp/security/audit.py +25 -0
  12. agentvee_mcp-0.1.2/agentvee_mcp/security/metrics.py +66 -0
  13. agentvee_mcp-0.1.2/agentvee_mcp/security/sessions.py +9 -0
  14. agentvee_mcp-0.1.2/agentvee_mcp/security/throttle.py +56 -0
  15. agentvee_mcp-0.1.2/agentvee_mcp/security/wait_gate.py +37 -0
  16. agentvee_mcp-0.1.2/agentvee_mcp/server.py +21 -0
  17. agentvee_mcp-0.1.2/agentvee_mcp/stdio.py +49 -0
  18. agentvee_mcp-0.1.2/agentvee_mcp/tools/__init__.py +0 -0
  19. agentvee_mcp-0.1.2/agentvee_mcp/tools/download_url.py +67 -0
  20. agentvee_mcp-0.1.2/agentvee_mcp/tools/list_on_marketplace.py +95 -0
  21. agentvee_mcp-0.1.2/agentvee_mcp/tools/upload_and_wait.py +231 -0
  22. agentvee_mcp-0.1.2/agentvee_mcp/tools/upload_file.py +110 -0
  23. agentvee_mcp-0.1.2/agentvee_mcp/tools/upload_status.py +68 -0
  24. agentvee_mcp-0.1.2/agentvee_mcp/tools/upload_url.py +83 -0
  25. agentvee_mcp-0.1.2/agentvee_mcp/validation/__init__.py +0 -0
  26. agentvee_mcp-0.1.2/agentvee_mcp/validation/base64_content.py +52 -0
  27. agentvee_mcp-0.1.2/agentvee_mcp/validation/file_path.py +42 -0
  28. agentvee_mcp-0.1.2/agentvee_mcp/validation/upload_id.py +16 -0
  29. agentvee_mcp-0.1.2/agentvee_mcp/validation/url.py +40 -0
  30. agentvee_mcp-0.1.2/pyproject.toml +32 -0
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ dist/
3
+ *.egg-info/
4
+ __pycache__/
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentvee-mcp
3
+ Version: 0.1.2
4
+ Summary: MCP server for AgentVee — file uploads for AI agents
5
+ Project-URL: Homepage, https://agentvee.io
6
+ Project-URL: Repository, https://github.com/agentvee/agentvee
7
+ License-Expression: MIT
8
+ Keywords: agentvee,ai-agents,file-upload,mcp
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: httpx>=0.27.0
17
+ Requires-Dist: mcp>=1.0.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # crabtransfer-mcp
21
+
22
+ MCP server for [CrabTransfer](https://crabtransfer.com) — file uploads for AI agents with local `filePath` support.
23
+
24
+ > **Note:** This is currently a test version. Full launch coming soon.
25
+
26
+ ## Quick Start
27
+
28
+ 1. **Get an API key** at [crabtransfer.com/dashboard](https://crabtransfer.com/dashboard)
29
+
30
+ 1. **Add to your MCP config** (e.g. `~/.cursor/mcp.json`):
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "crabtransfer": {
36
+ "command": "uvx",
37
+ "args": ["crabtransfer-mcp"],
38
+ "env": {
39
+ "CRABTRANSFER_API_KEY": "your-api-key-here"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ Or with `pip`:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "crabtransfer": {
52
+ "command": "python",
53
+ "args": ["-m", "crabtransfer_mcp"],
54
+ "env": {
55
+ "CRABTRANSFER_API_KEY": "your-api-key-here"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ 1. **Restart your MCP client** (Cursor, Claude Desktop, etc.)
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install crabtransfer-mcp
68
+ ```
69
+
70
+ Or with uv:
71
+
72
+ ```bash
73
+ uvx crabtransfer-mcp --help
74
+ ```
75
+
76
+ ## Supported Clients
77
+
78
+ Any MCP client that supports stdio transport:
79
+
80
+ - [Cursor](https://cursor.sh)
81
+ - [Claude Desktop](https://claude.ai/download)
82
+ - [Cline](https://github.com/cline/cline)
83
+ - [Continue](https://continue.dev)
84
+
85
+ ## Available Tools
86
+
87
+ | Tool | Description |
88
+ | ------------------- | ---------------------------------------------------- |
89
+ | `upload_and_wait` | Upload a file and wait for a shareable download link |
90
+ | `upload_file` | Upload a file (returns upload ID for status polling) |
91
+ | `upload_from_url` | Upload a file from a public URL |
92
+ | `get_upload_status` | Check the status of an upload |
93
+ | `get_download_url` | Get the download URL for a completed upload |
94
+
95
+ ### Upload Methods
96
+
97
+ Each upload tool supports three mutually exclusive input methods:
98
+
99
+ - **`filePath`** — Local file path (stdio only, most efficient for local files — zero tokens)
100
+ - **`url`** — Public URL for the server to fetch
101
+ - **`content`** — Base64-encoded file content (fallback, uses tokens)
102
+
103
+ ## Environment Variables
104
+
105
+ | Variable | Required | Default | Description |
106
+ | --------------------------- | -------- | ------------------------------------------ | ------------------------- |
107
+ | `CRABTRANSFER_API_KEY` | Yes | — | Your CrabTransfer API key |
108
+ | `CRABTRANSFER_API_BASE_URL` | No | `https://crabtransfer-api-develop.fly.dev` | API base URL |
109
+
110
+ ## CLI
111
+
112
+ ```bash
113
+ crabtransfer-mcp --help
114
+ crabtransfer-mcp --version
115
+ ```
116
+
117
+ ## HTTP Transport
118
+
119
+ To run as an HTTP server (Streamable HTTP):
120
+
121
+ ```bash
122
+ crabtransfer-mcp-http
123
+ ```
124
+
125
+ Or:
126
+
127
+ ```bash
128
+ python -m crabtransfer_mcp.http
129
+ ```
130
+
131
+ ## Also Available
132
+
133
+ - **Node.js:** `npx -y @crabtransfer/mcp` ([npm](https://www.npmjs.com/package/@crabtransfer/mcp))
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,118 @@
1
+ # crabtransfer-mcp
2
+
3
+ MCP server for [CrabTransfer](https://crabtransfer.com) — file uploads for AI agents with local `filePath` support.
4
+
5
+ > **Note:** This is currently a test version. Full launch coming soon.
6
+
7
+ ## Quick Start
8
+
9
+ 1. **Get an API key** at [crabtransfer.com/dashboard](https://crabtransfer.com/dashboard)
10
+
11
+ 1. **Add to your MCP config** (e.g. `~/.cursor/mcp.json`):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "crabtransfer": {
17
+ "command": "uvx",
18
+ "args": ["crabtransfer-mcp"],
19
+ "env": {
20
+ "CRABTRANSFER_API_KEY": "your-api-key-here"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ Or with `pip`:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "crabtransfer": {
33
+ "command": "python",
34
+ "args": ["-m", "crabtransfer_mcp"],
35
+ "env": {
36
+ "CRABTRANSFER_API_KEY": "your-api-key-here"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ 1. **Restart your MCP client** (Cursor, Claude Desktop, etc.)
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install crabtransfer-mcp
49
+ ```
50
+
51
+ Or with uv:
52
+
53
+ ```bash
54
+ uvx crabtransfer-mcp --help
55
+ ```
56
+
57
+ ## Supported Clients
58
+
59
+ Any MCP client that supports stdio transport:
60
+
61
+ - [Cursor](https://cursor.sh)
62
+ - [Claude Desktop](https://claude.ai/download)
63
+ - [Cline](https://github.com/cline/cline)
64
+ - [Continue](https://continue.dev)
65
+
66
+ ## Available Tools
67
+
68
+ | Tool | Description |
69
+ | ------------------- | ---------------------------------------------------- |
70
+ | `upload_and_wait` | Upload a file and wait for a shareable download link |
71
+ | `upload_file` | Upload a file (returns upload ID for status polling) |
72
+ | `upload_from_url` | Upload a file from a public URL |
73
+ | `get_upload_status` | Check the status of an upload |
74
+ | `get_download_url` | Get the download URL for a completed upload |
75
+
76
+ ### Upload Methods
77
+
78
+ Each upload tool supports three mutually exclusive input methods:
79
+
80
+ - **`filePath`** — Local file path (stdio only, most efficient for local files — zero tokens)
81
+ - **`url`** — Public URL for the server to fetch
82
+ - **`content`** — Base64-encoded file content (fallback, uses tokens)
83
+
84
+ ## Environment Variables
85
+
86
+ | Variable | Required | Default | Description |
87
+ | --------------------------- | -------- | ------------------------------------------ | ------------------------- |
88
+ | `CRABTRANSFER_API_KEY` | Yes | — | Your CrabTransfer API key |
89
+ | `CRABTRANSFER_API_BASE_URL` | No | `https://crabtransfer-api-develop.fly.dev` | API base URL |
90
+
91
+ ## CLI
92
+
93
+ ```bash
94
+ crabtransfer-mcp --help
95
+ crabtransfer-mcp --version
96
+ ```
97
+
98
+ ## HTTP Transport
99
+
100
+ To run as an HTTP server (Streamable HTTP):
101
+
102
+ ```bash
103
+ crabtransfer-mcp-http
104
+ ```
105
+
106
+ Or:
107
+
108
+ ```bash
109
+ python -m crabtransfer_mcp.http
110
+ ```
111
+
112
+ ## Also Available
113
+
114
+ - **Node.js:** `npx -y @crabtransfer/mcp` ([npm](https://www.npmjs.com/package/@crabtransfer/mcp))
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,3 @@
1
+ """AgentVee MCP server — file uploads for AI agents."""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,3 @@
1
+ from agentvee_mcp.stdio import main
2
+
3
+ main()
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from .config import mcp_config
9
+
10
+
11
+ @dataclass
12
+ class ApiOk:
13
+ ok: bool
14
+ status: int
15
+ data: dict[str, Any]
16
+
17
+ def __init__(self, status: int, data: dict[str, Any]):
18
+ self.ok = True
19
+ self.status = status
20
+ self.data = data
21
+
22
+
23
+ @dataclass
24
+ class ApiErr:
25
+ ok: bool
26
+ status: int
27
+ code: str
28
+ message: str
29
+ retry_after_sec: int | None = None
30
+
31
+ def __init__(
32
+ self,
33
+ status: int,
34
+ code: str,
35
+ message: str,
36
+ retry_after_sec: int | None = None,
37
+ ):
38
+ self.ok = False
39
+ self.status = status
40
+ self.code = code
41
+ self.message = message
42
+ self.retry_after_sec = retry_after_sec
43
+
44
+
45
+ ApiResult = ApiOk | ApiErr
46
+
47
+
48
+ def _map_error_message(
49
+ status: int,
50
+ code: str | None,
51
+ server_message: str | None,
52
+ ) -> str:
53
+ mapping: dict[str, str] = {
54
+ "rate_limit_exceeded": server_message or "Rate limit exceeded. Please wait and retry.",
55
+ "unauthorized": "Invalid or missing API key.",
56
+ "forbidden": "API key does not have required permissions.",
57
+ "size_limit_exceeded": f"File exceeds {mcp_config.max_file_size_bytes // (1024 * 1024)} MB limit.",
58
+ "blocked_mime_type": server_message or "File type not allowed.",
59
+ "empty_file": "File cannot be empty.",
60
+ "upload_worker_unavailable": "Upload service temporarily unavailable. Please retry.",
61
+ "storage_degraded": "Storage backend temporarily degraded. Please retry in 30s.",
62
+ "concurrency_limit_reached": "Too many concurrent uploads. Please wait and retry.",
63
+ "idempotency_in_progress": "A request with this idempotency key is already being processed.",
64
+ "invalid_url": server_message or "URL is not allowed.",
65
+ "ssrf_blocked": server_message or "URL is not allowed.",
66
+ "fetch_failed": server_message or "Failed to fetch file from URL.",
67
+ "fetch_timeout": "Timed out fetching file from URL.",
68
+ "upload_not_found": "Upload not found.",
69
+ }
70
+
71
+ if code and code in mapping:
72
+ return mapping[code]
73
+ if status >= 500:
74
+ return "Upload service error. Please retry."
75
+ return server_message or f"Request failed ({status})."
76
+
77
+
78
+ def _parse_error(response: httpx.Response) -> ApiErr:
79
+ try:
80
+ body = response.json()
81
+ except Exception:
82
+ body = None
83
+
84
+ error_obj = (body or {}).get("error", {}) if isinstance(body, dict) else {}
85
+ code = error_obj.get("code") or f"http_{response.status_code}"
86
+ server_message = error_obj.get("message")
87
+
88
+ retry_after_sec = error_obj.get("retryAfterSec")
89
+ if retry_after_sec is None:
90
+ raw_retry = response.headers.get("Retry-After")
91
+ if raw_retry:
92
+ try:
93
+ retry_after_sec = int(raw_retry)
94
+ except ValueError:
95
+ pass
96
+
97
+ return ApiErr(
98
+ status=response.status_code,
99
+ code=code,
100
+ message=_map_error_message(response.status_code, code, server_message),
101
+ retry_after_sec=retry_after_sec,
102
+ )
103
+
104
+
105
+ def _client() -> httpx.AsyncClient:
106
+ return httpx.AsyncClient(
107
+ timeout=httpx.Timeout(mcp_config.api_timeout_ms / 1000),
108
+ )
109
+
110
+
111
+ async def api_get(path: str, agent_key: str) -> ApiResult:
112
+ url = f"{mcp_config.api_base_url}{path}"
113
+ async with _client() as client:
114
+ try:
115
+ resp = await client.get(url, headers={"X-Agent-Key": agent_key})
116
+ except httpx.TimeoutException:
117
+ return ApiErr(0, "timeout", f"Request timed out after {mcp_config.api_timeout_ms}ms")
118
+ except httpx.HTTPError as exc:
119
+ return ApiErr(0, "network_error", f"Upload service error: {exc}")
120
+
121
+ if resp.is_success:
122
+ return ApiOk(resp.status_code, resp.json())
123
+ return _parse_error(resp)
124
+
125
+
126
+ async def api_post_json(
127
+ path: str,
128
+ agent_key: str,
129
+ body: dict[str, Any],
130
+ extra_headers: dict[str, str] | None = None,
131
+ ) -> ApiResult:
132
+ url = f"{mcp_config.api_base_url}{path}"
133
+ headers: dict[str, str] = {"X-Agent-Key": agent_key, "Content-Type": "application/json"}
134
+ if extra_headers:
135
+ headers.update(extra_headers)
136
+
137
+ async with _client() as client:
138
+ try:
139
+ resp = await client.post(url, headers=headers, json=body)
140
+ except httpx.TimeoutException:
141
+ return ApiErr(0, "timeout", f"Request timed out after {mcp_config.api_timeout_ms}ms")
142
+ except httpx.HTTPError as exc:
143
+ return ApiErr(0, "network_error", f"Upload service error: {exc}")
144
+
145
+ if resp.is_success:
146
+ return ApiOk(resp.status_code, resp.json())
147
+ return _parse_error(resp)
148
+
149
+
150
+ async def api_post_multipart(
151
+ path: str,
152
+ agent_key: str,
153
+ file_data: bytes,
154
+ file_name: str,
155
+ mime_type: str = "application/octet-stream",
156
+ extra_headers: dict[str, str] | None = None,
157
+ ) -> ApiResult:
158
+ url = f"{mcp_config.api_base_url}{path}"
159
+ headers: dict[str, str] = {"X-Agent-Key": agent_key}
160
+ if extra_headers:
161
+ headers.update(extra_headers)
162
+
163
+ files = {"file": (file_name, file_data, mime_type)}
164
+
165
+ async with _client() as client:
166
+ try:
167
+ resp = await client.post(url, headers=headers, files=files)
168
+ except httpx.TimeoutException:
169
+ return ApiErr(0, "timeout", f"Request timed out after {mcp_config.api_timeout_ms}ms")
170
+ except httpx.HTTPError as exc:
171
+ return ApiErr(0, "network_error", f"Upload service error: {exc}")
172
+
173
+ if resp.is_success:
174
+ return ApiOk(resp.status_code, resp.json())
175
+ return _parse_error(resp)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class AuthContext:
9
+ agent_key: str
10
+ token_prefix: str
11
+
12
+
13
+ def resolve_auth() -> AuthContext | None:
14
+ """Resolve API key from environment variable (stdio transport)."""
15
+ env_key = (os.environ.get("AGENTVEE_API_KEY") or "").strip()
16
+ if env_key:
17
+ return AuthContext(agent_key=env_key, token_prefix=env_key[:12])
18
+ return None
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Literal
6
+
7
+ TransportMode = Literal["http", "stdio"]
8
+
9
+ _transport_mode: TransportMode = "http"
10
+
11
+
12
+ def set_transport_mode(mode: TransportMode) -> None:
13
+ global _transport_mode
14
+ _transport_mode = mode
15
+
16
+
17
+ def get_transport_mode() -> TransportMode:
18
+ return _transport_mode
19
+
20
+
21
+ def _optional_env(name: str, fallback: str) -> str:
22
+ val = (os.environ.get(name) or "").strip()
23
+ return val if val else fallback
24
+
25
+
26
+ def _number_env(name: str, fallback: int) -> int:
27
+ raw = os.environ.get(name)
28
+ if not raw:
29
+ return fallback
30
+ try:
31
+ parsed = int(raw)
32
+ return parsed if parsed > 0 else fallback
33
+ except ValueError:
34
+ return fallback
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ThrottleConfig:
39
+ upload_window_ms: int = 15 * 60 * 1000
40
+ upload_max_requests: int = 30
41
+ status_window_ms: int = 15 * 60 * 1000
42
+ status_max_requests: int = 120
43
+ download_url_window_ms: int = 15 * 60 * 1000
44
+ download_url_max_requests: int = 60
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class WaitConfig:
49
+ max_concurrent: int = 50
50
+ max_duration_ms: int = 120_000
51
+ poll_initial_ms: int = 2_000
52
+ poll_max_ms: int = 8_000
53
+ poll_backoff_factor: float = 1.5
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class SessionConfig:
58
+ max_total: int = 1000
59
+ max_per_token: int = 50
60
+ idle_timeout_ms: int = 5 * 60 * 1000
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class McpConfig:
65
+ api_base_url: str = ""
66
+ port: int = 8082
67
+ allowed_origins: list[str] = field(default_factory=list)
68
+ log_level: str = "info"
69
+ max_file_size_bytes: int = 5 * 1024 * 1024
70
+ api_timeout_ms: int = 60_000
71
+ metrics_token: str = ""
72
+ throttle: ThrottleConfig = field(default_factory=ThrottleConfig)
73
+ wait: WaitConfig = field(default_factory=WaitConfig)
74
+ session: SessionConfig = field(default_factory=SessionConfig)
75
+
76
+
77
+ def _build_config() -> McpConfig:
78
+ origins_raw = _optional_env("MCP_ALLOWED_ORIGINS", "")
79
+ origins = [o.strip() for o in origins_raw.split(",") if o.strip()]
80
+
81
+ return McpConfig(
82
+ api_base_url=_optional_env(
83
+ "AGENTVEE_API_BASE_URL",
84
+ "https://agentvee-api-develop.fly.dev",
85
+ ).rstrip("/"),
86
+ port=_number_env("MCP_PORT", 8082),
87
+ allowed_origins=origins,
88
+ log_level=_optional_env("MCP_LOG_LEVEL", "info"),
89
+ max_file_size_bytes=5 * 1024 * 1024,
90
+ api_timeout_ms=_number_env("MCP_API_TIMEOUT_MS", 60_000),
91
+ metrics_token=_optional_env("MCP_METRICS_TOKEN", ""),
92
+ throttle=ThrottleConfig(),
93
+ wait=WaitConfig(
94
+ max_concurrent=_number_env("MCP_WAIT_MAX_CONCURRENT", 50),
95
+ max_duration_ms=_number_env("MCP_WAIT_MAX_DURATION_MS", 120_000),
96
+ poll_initial_ms=_number_env("MCP_WAIT_POLL_INITIAL_MS", 2_000),
97
+ poll_max_ms=_number_env("MCP_WAIT_POLL_MAX_MS", 8_000),
98
+ ),
99
+ session=SessionConfig(
100
+ max_total=_number_env("MCP_SESSION_MAX_TOTAL", 1000),
101
+ max_per_token=_number_env("MCP_SESSION_MAX_PER_TOKEN", 50),
102
+ idle_timeout_ms=_number_env("MCP_SESSION_IDLE_TIMEOUT_MS", 5 * 60 * 1000),
103
+ ),
104
+ )
105
+
106
+
107
+ mcp_config = _build_config()
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from .config import mcp_config, set_transport_mode
6
+ from .security.audit import audit_log
7
+ from .server import create_mcp_server
8
+
9
+
10
+ def main() -> None:
11
+ set_transport_mode("http")
12
+ server = create_mcp_server()
13
+ audit_log({"event": "startup", "transport": "streamable-http", "port": mcp_config.port})
14
+ server.run(transport="streamable-http", host="0.0.0.0", port=mcp_config.port)
15
+
16
+
17
+ if __name__ == "__main__":
18
+ main()
File without changes
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+
9
+ def audit_log(entry: dict[str, Any]) -> None:
10
+ record = {"ts": datetime.now(timezone.utc).isoformat(), **entry}
11
+ print(json.dumps(record, default=str), file=sys.stderr, flush=True)
12
+
13
+
14
+ def sanitize_args(tool_name: str, args: dict[str, Any]) -> dict[str, Any]:
15
+ """Sanitize tool arguments for logging — never log tokens or large payloads."""
16
+ safe: dict[str, Any] = {}
17
+ for key, value in args.items():
18
+ if key == "content" and isinstance(value, str):
19
+ safe["contentSizeBytes"] = len(value) * 3 // 4
20
+ continue
21
+ if isinstance(value, str) and len(value) > 200:
22
+ safe[key] = f"{value[:50]}...[{len(value)} chars]"
23
+ continue
24
+ safe[key] = value
25
+ return safe
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ _started_at = time.monotonic()
8
+ _total_requests = 0
9
+ _auth_failures = 0
10
+ _throttle_rejections = 0
11
+
12
+
13
+ @dataclass
14
+ class _ToolMetrics:
15
+ calls: int = 0
16
+ errors: int = 0
17
+ total_latency_ms: float = 0
18
+
19
+
20
+ _tool_metrics: dict[str, _ToolMetrics] = {}
21
+
22
+
23
+ def _get_or_create(tool: str) -> _ToolMetrics:
24
+ if tool not in _tool_metrics:
25
+ _tool_metrics[tool] = _ToolMetrics()
26
+ return _tool_metrics[tool]
27
+
28
+
29
+ def record_tool_call(tool: str, duration_ms: float, is_error: bool) -> None:
30
+ global _total_requests
31
+ _total_requests += 1
32
+ m = _get_or_create(tool)
33
+ m.calls += 1
34
+ m.total_latency_ms += duration_ms
35
+ if is_error:
36
+ m.errors += 1
37
+
38
+
39
+ def record_auth_failure() -> None:
40
+ global _auth_failures
41
+ _auth_failures += 1
42
+
43
+
44
+ def record_throttle_rejection() -> None:
45
+ global _throttle_rejections
46
+ _throttle_rejections += 1
47
+
48
+
49
+ def metrics_snapshot(active_sessions: int = 0, active_waits: int = 0) -> dict[str, Any]:
50
+ tools: dict[str, Any] = {}
51
+ for name, m in _tool_metrics.items():
52
+ tools[name] = {
53
+ "calls": m.calls,
54
+ "errors": m.errors,
55
+ "avgLatencyMs": round(m.total_latency_ms / m.calls) if m.calls > 0 else 0,
56
+ }
57
+
58
+ return {
59
+ "uptimeSeconds": int(time.monotonic() - _started_at),
60
+ "activeSessions": active_sessions,
61
+ "activeWaits": active_waits,
62
+ "totalRequests": _total_requests,
63
+ "authFailures": _auth_failures,
64
+ "throttleRejections": _throttle_rejections,
65
+ "tools": tools,
66
+ }
@@ -0,0 +1,9 @@
1
+ """Session tracking placeholder for HTTP transport compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _session_count = 0
6
+
7
+
8
+ def session_count() -> int:
9
+ return _session_count