msclabs.html-creator-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.
- msclabs_html_creator_mcp_server-1.0.0/.gitignore +16 -0
- msclabs_html_creator_mcp_server-1.0.0/PKG-INFO +39 -0
- msclabs_html_creator_mcp_server-1.0.0/README.md +32 -0
- msclabs_html_creator_mcp_server-1.0.0/pyproject.toml +19 -0
- msclabs_html_creator_mcp_server-1.0.0/src/msclabs_html_creator_mcp_server/__init__.py +3 -0
- msclabs_html_creator_mcp_server-1.0.0/src/msclabs_html_creator_mcp_server/__main__.py +5 -0
- msclabs_html_creator_mcp_server-1.0.0/src/msclabs_html_creator_mcp_server/bridge.py +130 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: msclabs.html-creator-mcp-server
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Connects an MCP client over stdio to the html-creator assessment site workflow, hosted by the MSC Labs CoE.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# msclabs.html-creator-mcp-server
|
|
9
|
+
|
|
10
|
+
Connects an MCP client to the html-creator assessment site workflow, which runs in the MSC Labs CoE account. The workflow, its
|
|
11
|
+
guardrails and its policy live there; this package is only the stdio bridge.
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"msclabs.html-creator": {
|
|
19
|
+
"command": "uvx",
|
|
20
|
+
"args": ["msclabs.html-creator-mcp-server@latest"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Set `MCP_API_TOKEN` to the key the CoE issued you, and add `MCP_API_TOKEN` to your client's
|
|
27
|
+
approved environment variables. Without a key the registry answers 401 and nothing works.
|
|
28
|
+
|
|
29
|
+
## What it does not do
|
|
30
|
+
|
|
31
|
+
It holds no copy of the workflow, caches nothing and decides nothing. Every step, reference and
|
|
32
|
+
policy check is answered by the hosted server at call time, so what you run is always what the
|
|
33
|
+
CoE published.
|
|
34
|
+
|
|
35
|
+
| Variable | Default | Purpose |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `MCP_API_TOKEN` | — | Your API key. Required. |
|
|
38
|
+
| `MSCLABS_MCP_BASE_URL` | the CoE endpoint | Point at a different deployment. |
|
|
39
|
+
| `MSCLABS_MCP_TIMEOUT` | `120` | Seconds to wait for one call. |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# msclabs.html-creator-mcp-server
|
|
2
|
+
|
|
3
|
+
Connects an MCP client to the html-creator assessment site workflow, which runs in the MSC Labs CoE account. The workflow, its
|
|
4
|
+
guardrails and its policy live there; this package is only the stdio bridge.
|
|
5
|
+
|
|
6
|
+
## Configure
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"mcpServers": {
|
|
11
|
+
"msclabs.html-creator": {
|
|
12
|
+
"command": "uvx",
|
|
13
|
+
"args": ["msclabs.html-creator-mcp-server@latest"]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Set `MCP_API_TOKEN` to the key the CoE issued you, and add `MCP_API_TOKEN` to your client's
|
|
20
|
+
approved environment variables. Without a key the registry answers 401 and nothing works.
|
|
21
|
+
|
|
22
|
+
## What it does not do
|
|
23
|
+
|
|
24
|
+
It holds no copy of the workflow, caches nothing and decides nothing. Every step, reference and
|
|
25
|
+
policy check is answered by the hosted server at call time, so what you run is always what the
|
|
26
|
+
CoE published.
|
|
27
|
+
|
|
28
|
+
| Variable | Default | Purpose |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `MCP_API_TOKEN` | — | Your API key. Required. |
|
|
31
|
+
| `MSCLABS_MCP_BASE_URL` | the CoE endpoint | Point at a different deployment. |
|
|
32
|
+
| `MSCLABS_MCP_TIMEOUT` | `120` | Seconds to wait for one call. |
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "msclabs.html-creator-mcp-server"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Connects an MCP client over stdio to the html-creator assessment site workflow, hosted by the MSC Labs CoE."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
# The standard library only, on purpose: nothing third-party should ever see the API key, and
|
|
8
|
+
# `uvx` start-up stays the download of one small wheel.
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
"msclabs.html-creator-mcp-server" = "msclabs_html_creator_mcp_server.bridge:main"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/msclabs_html_creator_mcp_server"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""stdio to the hosted registry, and nothing else.
|
|
2
|
+
|
|
3
|
+
This is the only piece of the registry that runs on a developer's machine, so it is kept as
|
|
4
|
+
close to empty as a working program can be: it reads a JSON-RPC line from stdin, posts it to
|
|
5
|
+
the hosted server with the developer's API key, and writes the reply back to stdout. There is
|
|
6
|
+
no procedure here, no policy, no cached content and no state — those stay server-side, which is
|
|
7
|
+
the whole reason the registry is hosted rather than installed. A developer can read every line
|
|
8
|
+
of this file and find nothing to tune.
|
|
9
|
+
|
|
10
|
+
It depends on the standard library alone. That keeps `uvx` start-up to the download of one
|
|
11
|
+
small wheel, and keeps the API key out of reach of any third-party package.
|
|
12
|
+
|
|
13
|
+
Posting a line and reading the reply is enough because the hosted servers run in stateless JSON
|
|
14
|
+
mode: one request, one response, no session to keep alive and no event stream to multiplex.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.request
|
|
24
|
+
|
|
25
|
+
#: Filled in per distribution by the generator in `build_clients.py`.
|
|
26
|
+
SERVER_ID = "html-creator"
|
|
27
|
+
SERVER_NAME = "msclabs.html-creator"
|
|
28
|
+
BASE_URL = "https://k98rfq7exb.execute-api.ap-southeast-7.amazonaws.com"
|
|
29
|
+
VERSION = "1.0.0"
|
|
30
|
+
|
|
31
|
+
TOKEN_VARS = ("MCP_API_TOKEN", "MSCLABS_MCP_TOKEN")
|
|
32
|
+
TIMEOUT = float(os.environ.get("MSCLABS_MCP_TIMEOUT", "120"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _log(message: str) -> None:
|
|
36
|
+
"""Diagnostics go to stderr. stdout carries the protocol and nothing else."""
|
|
37
|
+
print(f"[{SERVER_NAME}] {message}", file=sys.stderr, flush=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _token() -> str:
|
|
41
|
+
for name in TOKEN_VARS:
|
|
42
|
+
value = os.environ.get(name, "").strip()
|
|
43
|
+
if value:
|
|
44
|
+
return value
|
|
45
|
+
_log(
|
|
46
|
+
"No API key. Set MCP_API_TOKEN to the key the CoE issued you, and add MCP_API_TOKEN to "
|
|
47
|
+
"your client's approved environment variables. Ask the CoE for a key if you have none."
|
|
48
|
+
)
|
|
49
|
+
raise SystemExit(2)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _endpoint() -> str:
|
|
53
|
+
base = os.environ.get("MSCLABS_MCP_BASE_URL", BASE_URL).rstrip("/")
|
|
54
|
+
return f"{base}/{SERVER_ID}/mcp"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _error(request_id: object, code: int, message: str) -> str:
|
|
58
|
+
return json.dumps({"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _forward(url: str, token: str, line: str, request_id: object) -> str | None:
|
|
62
|
+
"""Posts one message upstream and returns the reply to write to stdout, if any."""
|
|
63
|
+
request = urllib.request.Request(url, data=line.encode(), method="POST")
|
|
64
|
+
request.add_header("Content-Type", "application/json")
|
|
65
|
+
request.add_header("Accept", "application/json, text/event-stream")
|
|
66
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
67
|
+
request.add_header("User-Agent", f"{SERVER_NAME}/{VERSION}")
|
|
68
|
+
try:
|
|
69
|
+
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
|
|
70
|
+
body = response.read().decode(errors="replace")
|
|
71
|
+
except urllib.error.HTTPError as exc:
|
|
72
|
+
detail = exc.read().decode(errors="replace")[:400]
|
|
73
|
+
if exc.code in (401, 403):
|
|
74
|
+
# The most common failure by far, and the one whose default message sends people
|
|
75
|
+
# looking in the wrong place: the client reports an OAuth problem when the real
|
|
76
|
+
# cause is a key that is missing, mistyped or revoked.
|
|
77
|
+
_log(
|
|
78
|
+
f"{exc.code} from the registry: the API key was rejected. Check MCP_API_TOKEN is "
|
|
79
|
+
"the whole key, that your client is allowed to pass it through, and that the key "
|
|
80
|
+
"has not been revoked."
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
_log(f"{exc.code} from the registry: {detail}")
|
|
84
|
+
if request_id is None:
|
|
85
|
+
return None
|
|
86
|
+
return _error(request_id, -32000, f"registry returned HTTP {exc.code}")
|
|
87
|
+
except Exception as exc: # noqa: BLE001 - any transport failure becomes one JSON-RPC error
|
|
88
|
+
_log(f"could not reach the registry: {exc}")
|
|
89
|
+
if request_id is None:
|
|
90
|
+
return None
|
|
91
|
+
return _error(request_id, -32000, f"could not reach the registry: {exc}")
|
|
92
|
+
|
|
93
|
+
if request_id is None:
|
|
94
|
+
# A notification gets no reply, and writing one would desynchronise the client.
|
|
95
|
+
return None
|
|
96
|
+
# Stateless JSON mode answers with a JSON object; an SSE frame is still accepted in case a
|
|
97
|
+
# future deployment negotiates one.
|
|
98
|
+
for candidate in body.splitlines():
|
|
99
|
+
candidate = candidate.removeprefix("data:").strip()
|
|
100
|
+
if candidate.startswith("{"):
|
|
101
|
+
return candidate
|
|
102
|
+
return _error(request_id, -32000, "the registry returned an empty response")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> int:
|
|
106
|
+
token = _token()
|
|
107
|
+
url = _endpoint()
|
|
108
|
+
_log(f"bridging stdio to {url}")
|
|
109
|
+
|
|
110
|
+
for line in sys.stdin:
|
|
111
|
+
line = line.strip()
|
|
112
|
+
if not line:
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
message = json.loads(line)
|
|
116
|
+
except json.JSONDecodeError:
|
|
117
|
+
sys.stdout.write(_error(None, -32700, "parse error") + "\n")
|
|
118
|
+
sys.stdout.flush()
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
request_id = message.get("id") if isinstance(message, dict) else None
|
|
122
|
+
reply = _forward(url, token, line, request_id)
|
|
123
|
+
if reply is not None:
|
|
124
|
+
sys.stdout.write(reply + "\n")
|
|
125
|
+
sys.stdout.flush()
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
raise SystemExit(main())
|