wrg-mcp-server 1.0.0__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.
- wrg_mcp_server-1.0.0/PKG-INFO +156 -0
- wrg_mcp_server-1.0.0/README.md +125 -0
- wrg_mcp_server-1.0.0/pyproject.toml +65 -0
- wrg_mcp_server-1.0.0/setup.cfg +4 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server/__init__.py +4 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server/cli.py +50 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server/config.py +153 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server/http_utils.py +70 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server/local_tools.py +587 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server/server.py +181 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server.egg-info/PKG-INFO +156 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server.egg-info/SOURCES.txt +24 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server.egg-info/dependency_links.txt +1 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server.egg-info/entry_points.txt +2 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server.egg-info/requires.txt +8 -0
- wrg_mcp_server-1.0.0/src/wrg_mcp_server.egg-info/top_level.txt +1 -0
- wrg_mcp_server-1.0.0/tests/test_cli.py +91 -0
- wrg_mcp_server-1.0.0/tests/test_config.py +32 -0
- wrg_mcp_server-1.0.0/tests/test_config_edge.py +192 -0
- wrg_mcp_server-1.0.0/tests/test_http_utils.py +33 -0
- wrg_mcp_server-1.0.0/tests/test_http_utils_edge.py +143 -0
- wrg_mcp_server-1.0.0/tests/test_local_tools.py +188 -0
- wrg_mcp_server-1.0.0/tests/test_new_tools.py +331 -0
- wrg_mcp_server-1.0.0/tests/test_scaffold.py +8 -0
- wrg_mcp_server-1.0.0/tests/test_server_extras.py +103 -0
- wrg_mcp_server-1.0.0/tests/test_smoke.py +13 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wrg_mcp_server
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: WRG MCP server — exposes WinstonRedGuard tools to Claude and AI agents
|
|
5
|
+
Author-email: Yakuphan Yucel <yakuphan.yucel11@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yakuphanycl/WinstonRedGuard/tree/main/apps/wrg_mcp_server
|
|
8
|
+
Project-URL: Repository, https://github.com/yakuphanycl/WinstonRedGuard
|
|
9
|
+
Project-URL: Issues, https://github.com/yakuphanycl/WinstonRedGuard/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/yakuphanycl/WinstonRedGuard/tree/main/apps/wrg_mcp_server#readme
|
|
11
|
+
Keywords: ai-agents,claude,mcp,model-context-protocol,winstonredguard
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: mcp<2.0,>=1.0.0
|
|
26
|
+
Provides-Extra: remote
|
|
27
|
+
Requires-Dist: httpx<1.0,>=0.27; extra == "remote"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
31
|
+
|
|
32
|
+
<!-- mcp-name: io.github.yakuphanycl/wrg-mcp-server -->
|
|
33
|
+
|
|
34
|
+
# wrg_mcp_server
|
|
35
|
+
|
|
36
|
+
MCP (Model Context Protocol) server exposing the WinstonRedGuard monorepo to Claude and other MCP-compatible AI agents. Built on `FastMCP` — registers tools from every active WRG app so an agent can inspect the repo, run pipelines, query memory, and call remote services without shelling out.
|
|
37
|
+
|
|
38
|
+
## Transports
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
wrg-mcp-server --transport stdio # Claude Desktop / Claude Code
|
|
42
|
+
wrg-mcp-server --transport streamable-http # default HTTP (recommended)
|
|
43
|
+
wrg-mcp-server --transport sse # legacy HTTP
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Flags: `--host 0.0.0.0` · `--port 8080` · `--mcp-path /mcp`
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd apps/wrg_mcp_server
|
|
52
|
+
pip install -e . # core: MCP + local tools only
|
|
53
|
+
pip install -e ".[remote]" # adds httpx for site_* / pulseboard_* tools
|
|
54
|
+
pip install -e ".[dev]" # pytest + pytest-asyncio
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tools exposed
|
|
58
|
+
|
|
59
|
+
### Local (subprocess-backed, always available)
|
|
60
|
+
|
|
61
|
+
| Tool | What it does |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `connector_status` | Report which remote services are configured |
|
|
64
|
+
| `app_list`, `app_info` | Query `app_registry/data/registry.json` |
|
|
65
|
+
| `governance_run` | Execute `governance_check` across one or all apps |
|
|
66
|
+
| `release_check` | Run the `tools/release_check.ps1` gate |
|
|
67
|
+
| `pipeline_list`, `pipeline_show`, `pipeline_run` | `wrg_pipeline` DAG operations |
|
|
68
|
+
| `pulse_check` | Invoke `wrg-pulse check` |
|
|
69
|
+
| `memory_get`, `memory_set`, `memory_list`, `memory_search` | `wrg_memory` key-value access |
|
|
70
|
+
| `research_history`, `research_report`, `research_scan`, `research_watch`, `research_scan_summary` | `research_motor` runs and artifacts |
|
|
71
|
+
| `vault_audit` | `wrg_vault` audit ledger inspection |
|
|
72
|
+
| `scheduler_task_list`, `scheduler_tick_dry_run` | `wrg_scheduler` inspection |
|
|
73
|
+
|
|
74
|
+
### Remote (HTTP, opt-in via env)
|
|
75
|
+
|
|
76
|
+
| Tool | Upstream |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `site_health`, `site_get`, `site_post` | Company site API (`WRG_SITE_BASE_URL`) |
|
|
79
|
+
| `pulseboard_health`, `pulseboard_list_repos`, `pulseboard_add_repo`, `pulseboard_delete_repo`, `pulseboard_get_pulse` | `pulseboard` dashboard (`WRG_PULSEBOARD_BASE_URL`) |
|
|
80
|
+
|
|
81
|
+
Remote tools return `{"ok": false, "error": "httpx not installed — remote tools unavailable"}` when the `[remote]` extra is not installed.
|
|
82
|
+
|
|
83
|
+
## Environment
|
|
84
|
+
|
|
85
|
+
### Repo discovery
|
|
86
|
+
|
|
87
|
+
| Variable | Default | Purpose |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `WRG_REPO_ROOT` | auto-detect (walk up until `apps/` + `CLAUDE.md`) | Required when installed from wheel outside the monorepo |
|
|
90
|
+
|
|
91
|
+
### Mutation gate (default: off)
|
|
92
|
+
|
|
93
|
+
State-changing tools (`memory_set`, `pipeline_run`) refuse to execute unless:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
WRG_MCP_ALLOW_MUTATIONS=1
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This prevents an MCP client from silently writing memory or launching pipelines on a read-only connection.
|
|
100
|
+
|
|
101
|
+
### Remote service config
|
|
102
|
+
|
|
103
|
+
Per service (`SITE` / `PULSEBOARD`), prefix with `WRG_<SERVICE>_`:
|
|
104
|
+
|
|
105
|
+
| Variable | Default | Purpose |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `*_BASE_URL` | — | Enables the service (unset = service disabled) |
|
|
108
|
+
| `*_TOKEN` | — | Bearer token for `Authorization` header |
|
|
109
|
+
| `*_AUTH_HEADER` | `Authorization` | Override header name |
|
|
110
|
+
| `*_AUTH_SCHEME` | `Bearer` | Override token scheme |
|
|
111
|
+
| `*_SESSION_COOKIE` | — | Optional `Cookie` header |
|
|
112
|
+
| `*_EXTRA_HEADERS` | — | JSON object of extra headers |
|
|
113
|
+
| `*_TIMEOUT_SECONDS` | `WRG_HTTP_TIMEOUT_SECONDS` (20.0) | Per-request timeout |
|
|
114
|
+
| `*_VERIFY_TLS` | `WRG_HTTP_VERIFY_TLS` (true) | TLS verification |
|
|
115
|
+
|
|
116
|
+
## Claude Code / Claude Desktop integration
|
|
117
|
+
|
|
118
|
+
Add to your MCP client config:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"wrg": {
|
|
124
|
+
"command": "wrg-mcp-server",
|
|
125
|
+
"args": ["--transport", "stdio"],
|
|
126
|
+
"env": {
|
|
127
|
+
"WRG_REPO_ROOT": "D:\\dev\\WinstonRedGuard",
|
|
128
|
+
"WRG_MCP_ALLOW_MUTATIONS": "0"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Architecture
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
FastMCP server
|
|
139
|
+
├── server.py — tool registration, remote HTTP dispatch
|
|
140
|
+
├── config.py — ServiceConfig / AppConfig from env (frozen dataclasses)
|
|
141
|
+
├── http_utils.py — URL builder, response parser
|
|
142
|
+
├── local_tools.py — subprocess wrappers for WRG CLIs (~20 tools)
|
|
143
|
+
└── cli.py — argparse entry point
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Local tools use `subprocess.run` with `stdin=DEVNULL` (not asyncio subprocess) — avoids a Windows pipe-blocking deadlock under anyio. Tool dispatch is wrapped in `anyio.to_thread.run_sync` so the MCP event loop stays responsive.
|
|
147
|
+
|
|
148
|
+
## Tests
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pytest -q
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Status
|
|
155
|
+
|
|
156
|
+
Production — 1045 lines, covers every active WRG app, drives the `mcp__wrg__*` tools visible in connected Claude sessions.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.yakuphanycl/wrg-mcp-server -->
|
|
2
|
+
|
|
3
|
+
# wrg_mcp_server
|
|
4
|
+
|
|
5
|
+
MCP (Model Context Protocol) server exposing the WinstonRedGuard monorepo to Claude and other MCP-compatible AI agents. Built on `FastMCP` — registers tools from every active WRG app so an agent can inspect the repo, run pipelines, query memory, and call remote services without shelling out.
|
|
6
|
+
|
|
7
|
+
## Transports
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
wrg-mcp-server --transport stdio # Claude Desktop / Claude Code
|
|
11
|
+
wrg-mcp-server --transport streamable-http # default HTTP (recommended)
|
|
12
|
+
wrg-mcp-server --transport sse # legacy HTTP
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Flags: `--host 0.0.0.0` · `--port 8080` · `--mcp-path /mcp`
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd apps/wrg_mcp_server
|
|
21
|
+
pip install -e . # core: MCP + local tools only
|
|
22
|
+
pip install -e ".[remote]" # adds httpx for site_* / pulseboard_* tools
|
|
23
|
+
pip install -e ".[dev]" # pytest + pytest-asyncio
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Tools exposed
|
|
27
|
+
|
|
28
|
+
### Local (subprocess-backed, always available)
|
|
29
|
+
|
|
30
|
+
| Tool | What it does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `connector_status` | Report which remote services are configured |
|
|
33
|
+
| `app_list`, `app_info` | Query `app_registry/data/registry.json` |
|
|
34
|
+
| `governance_run` | Execute `governance_check` across one or all apps |
|
|
35
|
+
| `release_check` | Run the `tools/release_check.ps1` gate |
|
|
36
|
+
| `pipeline_list`, `pipeline_show`, `pipeline_run` | `wrg_pipeline` DAG operations |
|
|
37
|
+
| `pulse_check` | Invoke `wrg-pulse check` |
|
|
38
|
+
| `memory_get`, `memory_set`, `memory_list`, `memory_search` | `wrg_memory` key-value access |
|
|
39
|
+
| `research_history`, `research_report`, `research_scan`, `research_watch`, `research_scan_summary` | `research_motor` runs and artifacts |
|
|
40
|
+
| `vault_audit` | `wrg_vault` audit ledger inspection |
|
|
41
|
+
| `scheduler_task_list`, `scheduler_tick_dry_run` | `wrg_scheduler` inspection |
|
|
42
|
+
|
|
43
|
+
### Remote (HTTP, opt-in via env)
|
|
44
|
+
|
|
45
|
+
| Tool | Upstream |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `site_health`, `site_get`, `site_post` | Company site API (`WRG_SITE_BASE_URL`) |
|
|
48
|
+
| `pulseboard_health`, `pulseboard_list_repos`, `pulseboard_add_repo`, `pulseboard_delete_repo`, `pulseboard_get_pulse` | `pulseboard` dashboard (`WRG_PULSEBOARD_BASE_URL`) |
|
|
49
|
+
|
|
50
|
+
Remote tools return `{"ok": false, "error": "httpx not installed — remote tools unavailable"}` when the `[remote]` extra is not installed.
|
|
51
|
+
|
|
52
|
+
## Environment
|
|
53
|
+
|
|
54
|
+
### Repo discovery
|
|
55
|
+
|
|
56
|
+
| Variable | Default | Purpose |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `WRG_REPO_ROOT` | auto-detect (walk up until `apps/` + `CLAUDE.md`) | Required when installed from wheel outside the monorepo |
|
|
59
|
+
|
|
60
|
+
### Mutation gate (default: off)
|
|
61
|
+
|
|
62
|
+
State-changing tools (`memory_set`, `pipeline_run`) refuse to execute unless:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
WRG_MCP_ALLOW_MUTATIONS=1
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This prevents an MCP client from silently writing memory or launching pipelines on a read-only connection.
|
|
69
|
+
|
|
70
|
+
### Remote service config
|
|
71
|
+
|
|
72
|
+
Per service (`SITE` / `PULSEBOARD`), prefix with `WRG_<SERVICE>_`:
|
|
73
|
+
|
|
74
|
+
| Variable | Default | Purpose |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `*_BASE_URL` | — | Enables the service (unset = service disabled) |
|
|
77
|
+
| `*_TOKEN` | — | Bearer token for `Authorization` header |
|
|
78
|
+
| `*_AUTH_HEADER` | `Authorization` | Override header name |
|
|
79
|
+
| `*_AUTH_SCHEME` | `Bearer` | Override token scheme |
|
|
80
|
+
| `*_SESSION_COOKIE` | — | Optional `Cookie` header |
|
|
81
|
+
| `*_EXTRA_HEADERS` | — | JSON object of extra headers |
|
|
82
|
+
| `*_TIMEOUT_SECONDS` | `WRG_HTTP_TIMEOUT_SECONDS` (20.0) | Per-request timeout |
|
|
83
|
+
| `*_VERIFY_TLS` | `WRG_HTTP_VERIFY_TLS` (true) | TLS verification |
|
|
84
|
+
|
|
85
|
+
## Claude Code / Claude Desktop integration
|
|
86
|
+
|
|
87
|
+
Add to your MCP client config:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"wrg": {
|
|
93
|
+
"command": "wrg-mcp-server",
|
|
94
|
+
"args": ["--transport", "stdio"],
|
|
95
|
+
"env": {
|
|
96
|
+
"WRG_REPO_ROOT": "D:\\dev\\WinstonRedGuard",
|
|
97
|
+
"WRG_MCP_ALLOW_MUTATIONS": "0"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
FastMCP server
|
|
108
|
+
├── server.py — tool registration, remote HTTP dispatch
|
|
109
|
+
├── config.py — ServiceConfig / AppConfig from env (frozen dataclasses)
|
|
110
|
+
├── http_utils.py — URL builder, response parser
|
|
111
|
+
├── local_tools.py — subprocess wrappers for WRG CLIs (~20 tools)
|
|
112
|
+
└── cli.py — argparse entry point
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Local tools use `subprocess.run` with `stdin=DEVNULL` (not asyncio subprocess) — avoids a Windows pipe-blocking deadlock under anyio. Tool dispatch is wrapped in `anyio.to_thread.run_sync` so the MCP event loop stays responsive.
|
|
116
|
+
|
|
117
|
+
## Tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pytest -q
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Status
|
|
124
|
+
|
|
125
|
+
Production — 1045 lines, covers every active WRG app, drives the `mcp__wrg__*` tools visible in connected Claude sessions.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wrg_mcp_server"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "WRG MCP server — exposes WinstonRedGuard tools to Claude and AI agents"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Yakuphan Yucel", email = "yakuphan.yucel11@gmail.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"ai-agents",
|
|
15
|
+
"claude",
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"winstonredguard",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 5 - Production/Stable",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Software Development",
|
|
30
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
31
|
+
"Topic :: Utilities",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"mcp>=1.0.0,<2.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/yakuphanycl/WinstonRedGuard/tree/main/apps/wrg_mcp_server"
|
|
39
|
+
Repository = "https://github.com/yakuphanycl/WinstonRedGuard"
|
|
40
|
+
Issues = "https://github.com/yakuphanycl/WinstonRedGuard/issues"
|
|
41
|
+
Documentation = "https://github.com/yakuphanycl/WinstonRedGuard/tree/main/apps/wrg_mcp_server#readme"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
remote = [
|
|
45
|
+
"httpx>=0.27,<1.0",
|
|
46
|
+
]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=9.0.3",
|
|
49
|
+
"pytest-asyncio>=0.24",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
wrg-mcp-server = "wrg_mcp_server.cli:main"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
asyncio_mode = "auto"
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report]
|
|
63
|
+
# Baseline no-regression floor. Apps above this today (measured 2026-04-21)
|
|
64
|
+
# shouldn't drop below it; raise individually once they stabilise higher.
|
|
65
|
+
fail_under = 60
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI entrypoint for wrg_mcp_server.
|
|
2
|
+
|
|
3
|
+
Supports three transports:
|
|
4
|
+
stdio — for Claude Desktop / Claude Code integration
|
|
5
|
+
streamable-http — recommended HTTP transport (default)
|
|
6
|
+
sse — legacy HTTP transport
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="wrg-mcp-server",
|
|
18
|
+
description="WinstonRedGuard MCP server",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--transport",
|
|
22
|
+
choices=["stdio", "streamable-http", "sse"],
|
|
23
|
+
default="streamable-http",
|
|
24
|
+
help="MCP transport (default: streamable-http)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
|
|
27
|
+
parser.add_argument("--port", type=int, default=8080, help="Bind port (default: 8080)")
|
|
28
|
+
parser.add_argument("--mcp-path", default="/mcp", help="HTTP endpoint path (default: /mcp)")
|
|
29
|
+
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
from wrg_mcp_server.server import create_mcp_server
|
|
33
|
+
|
|
34
|
+
server = create_mcp_server(
|
|
35
|
+
host=args.host,
|
|
36
|
+
port=args.port,
|
|
37
|
+
streamable_http_path=args.mcp_path,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
print(
|
|
41
|
+
f"wrg-mcp-server starting (transport={args.transport}, "
|
|
42
|
+
f"host={args.host}, port={args.port})",
|
|
43
|
+
file=sys.stderr,
|
|
44
|
+
)
|
|
45
|
+
server.run(transport=args.transport)
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Environment-driven configuration for wrg_mcp_server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigError(RuntimeError):
|
|
11
|
+
"""Raised when connector configuration is invalid."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_bool(value: str | None, *, default: bool) -> bool:
|
|
15
|
+
if value is None:
|
|
16
|
+
return default
|
|
17
|
+
normalized = value.strip().lower()
|
|
18
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
19
|
+
return True
|
|
20
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
21
|
+
return False
|
|
22
|
+
raise ConfigError(f"Invalid boolean value: {value!r}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_float(value: str | None, *, default: float, key: str) -> float:
|
|
26
|
+
if value is None or not value.strip():
|
|
27
|
+
return default
|
|
28
|
+
try:
|
|
29
|
+
parsed = float(value)
|
|
30
|
+
except ValueError as exc:
|
|
31
|
+
raise ConfigError(f"{key} must be a number: {value!r}") from exc
|
|
32
|
+
if parsed <= 0:
|
|
33
|
+
raise ConfigError(f"{key} must be > 0: {value!r}")
|
|
34
|
+
return parsed
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_headers_json(raw: str | None, *, key: str) -> dict[str, str]:
|
|
38
|
+
if raw is None or not raw.strip():
|
|
39
|
+
return {}
|
|
40
|
+
try:
|
|
41
|
+
obj = json.loads(raw)
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise ConfigError(f"{key} must be valid JSON object") from exc
|
|
44
|
+
|
|
45
|
+
if not isinstance(obj, dict):
|
|
46
|
+
raise ConfigError(f"{key} must be a JSON object")
|
|
47
|
+
|
|
48
|
+
out: dict[str, str] = {}
|
|
49
|
+
for k, v in obj.items():
|
|
50
|
+
if not isinstance(k, str):
|
|
51
|
+
raise ConfigError(f"{key} keys must be strings")
|
|
52
|
+
if not isinstance(v, str):
|
|
53
|
+
raise ConfigError(f"{key} values must be strings")
|
|
54
|
+
out[k] = v
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class ServiceConfig:
|
|
60
|
+
"""Configuration for one upstream service."""
|
|
61
|
+
|
|
62
|
+
base_url: str
|
|
63
|
+
token: str | None
|
|
64
|
+
auth_header: str
|
|
65
|
+
auth_scheme: str
|
|
66
|
+
session_cookie: str | None
|
|
67
|
+
extra_headers: dict[str, str]
|
|
68
|
+
timeout_seconds: float
|
|
69
|
+
verify_tls: bool
|
|
70
|
+
|
|
71
|
+
def build_headers(self) -> dict[str, str]:
|
|
72
|
+
headers: dict[str, str] = dict(self.extra_headers)
|
|
73
|
+
if self.token:
|
|
74
|
+
token_value = f"{self.auth_scheme} {self.token}".strip()
|
|
75
|
+
headers[self.auth_header] = token_value
|
|
76
|
+
if self.session_cookie:
|
|
77
|
+
headers["Cookie"] = self.session_cookie
|
|
78
|
+
return headers
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class AppConfig:
|
|
83
|
+
"""Top-level connector configuration."""
|
|
84
|
+
|
|
85
|
+
site: ServiceConfig | None
|
|
86
|
+
pulseboard: ServiceConfig | None
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_env(cls, env: Mapping[str, str]) -> "AppConfig":
|
|
90
|
+
default_timeout = _parse_float(
|
|
91
|
+
env.get("WRG_HTTP_TIMEOUT_SECONDS"),
|
|
92
|
+
default=20.0,
|
|
93
|
+
key="WRG_HTTP_TIMEOUT_SECONDS",
|
|
94
|
+
)
|
|
95
|
+
default_verify_tls = _parse_bool(
|
|
96
|
+
env.get("WRG_HTTP_VERIFY_TLS"),
|
|
97
|
+
default=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
site = _service_from_env(
|
|
101
|
+
env=env,
|
|
102
|
+
prefix="WRG_SITE",
|
|
103
|
+
base_url=env.get("WRG_SITE_BASE_URL"),
|
|
104
|
+
default_timeout=default_timeout,
|
|
105
|
+
default_verify_tls=default_verify_tls,
|
|
106
|
+
)
|
|
107
|
+
pulseboard = _service_from_env(
|
|
108
|
+
env=env,
|
|
109
|
+
prefix="WRG_PULSEBOARD",
|
|
110
|
+
base_url=env.get("WRG_PULSEBOARD_BASE_URL"),
|
|
111
|
+
default_timeout=default_timeout,
|
|
112
|
+
default_verify_tls=default_verify_tls,
|
|
113
|
+
)
|
|
114
|
+
return cls(site=site, pulseboard=pulseboard)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _service_from_env(
|
|
118
|
+
*,
|
|
119
|
+
env: Mapping[str, str],
|
|
120
|
+
prefix: str,
|
|
121
|
+
base_url: str | None,
|
|
122
|
+
default_timeout: float,
|
|
123
|
+
default_verify_tls: bool,
|
|
124
|
+
) -> ServiceConfig | None:
|
|
125
|
+
if base_url is None or not base_url.strip():
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
timeout_seconds = _parse_float(
|
|
129
|
+
env.get(f"{prefix}_TIMEOUT_SECONDS"),
|
|
130
|
+
default=default_timeout,
|
|
131
|
+
key=f"{prefix}_TIMEOUT_SECONDS",
|
|
132
|
+
)
|
|
133
|
+
verify_tls = _parse_bool(
|
|
134
|
+
env.get(f"{prefix}_VERIFY_TLS"),
|
|
135
|
+
default=default_verify_tls,
|
|
136
|
+
)
|
|
137
|
+
extra_headers = _parse_headers_json(
|
|
138
|
+
env.get(f"{prefix}_EXTRA_HEADERS"),
|
|
139
|
+
key=f"{prefix}_EXTRA_HEADERS",
|
|
140
|
+
)
|
|
141
|
+
auth_scheme = env.get(f"{prefix}_AUTH_SCHEME", "Bearer").strip()
|
|
142
|
+
auth_header = env.get(f"{prefix}_AUTH_HEADER", "Authorization").strip()
|
|
143
|
+
|
|
144
|
+
return ServiceConfig(
|
|
145
|
+
base_url=base_url.rstrip("/"),
|
|
146
|
+
token=(env.get(f"{prefix}_TOKEN") or "").strip() or None,
|
|
147
|
+
auth_header=auth_header or "Authorization",
|
|
148
|
+
auth_scheme=auth_scheme,
|
|
149
|
+
session_cookie=(env.get(f"{prefix}_SESSION_COOKIE") or "").strip() or None,
|
|
150
|
+
extra_headers=extra_headers,
|
|
151
|
+
timeout_seconds=timeout_seconds,
|
|
152
|
+
verify_tls=verify_tls,
|
|
153
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""HTTP utility helpers.
|
|
2
|
+
|
|
3
|
+
These are only used when httpx is installed (for remote site/pulseboard tools).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
from urllib.parse import parse_qsl, urlencode, urljoin, urlparse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_relative_path(path: str) -> str:
|
|
13
|
+
"""Normalize user path and reject absolute/unsafe input."""
|
|
14
|
+
clean = path.strip()
|
|
15
|
+
if not clean:
|
|
16
|
+
return "/"
|
|
17
|
+
if "://" in clean:
|
|
18
|
+
raise ValueError("Absolute URLs are not allowed; provide a relative path.")
|
|
19
|
+
if clean.startswith("//"):
|
|
20
|
+
raise ValueError("Path must not start with //.")
|
|
21
|
+
if not clean.startswith("/"):
|
|
22
|
+
clean = f"/{clean}"
|
|
23
|
+
return clean
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_url(base_url: str, path: str, query: Mapping[str, Any] | None = None) -> str:
|
|
27
|
+
"""Build a final URL from base and safe relative path."""
|
|
28
|
+
normalized = normalize_relative_path(path)
|
|
29
|
+
parsed = urlparse(normalized)
|
|
30
|
+
if parsed.scheme or parsed.netloc:
|
|
31
|
+
raise ValueError("Path must be relative.")
|
|
32
|
+
|
|
33
|
+
absolute = urljoin(f"{base_url.rstrip('/')}/", parsed.path.lstrip("/"))
|
|
34
|
+
|
|
35
|
+
merged: dict[str, str] = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
|
36
|
+
if query:
|
|
37
|
+
for key, value in query.items():
|
|
38
|
+
if value is None:
|
|
39
|
+
continue
|
|
40
|
+
merged[str(key)] = str(value)
|
|
41
|
+
if not merged:
|
|
42
|
+
return absolute
|
|
43
|
+
return f"{absolute}?{urlencode(merged)}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_response(response: Any) -> dict[str, Any]:
|
|
47
|
+
"""Parse httpx response body in a tool-friendly format."""
|
|
48
|
+
content_type = response.headers.get("content-type", "")
|
|
49
|
+
body: Any
|
|
50
|
+
if "application/json" in content_type.lower():
|
|
51
|
+
try:
|
|
52
|
+
body = response.json()
|
|
53
|
+
except ValueError:
|
|
54
|
+
body = _truncate_text(response.text)
|
|
55
|
+
else:
|
|
56
|
+
body = _truncate_text(response.text)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"ok": response.is_success,
|
|
60
|
+
"status_code": response.status_code,
|
|
61
|
+
"url": str(response.url),
|
|
62
|
+
"content_type": content_type,
|
|
63
|
+
"body": body,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _truncate_text(value: str, max_chars: int = 4000) -> str:
|
|
68
|
+
if len(value) <= max_chars:
|
|
69
|
+
return value
|
|
70
|
+
return f"{value[:max_chars]}...(truncated)"
|