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.
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ build/
8
+ dist/
9
+ .aws-sam/
10
+ samconfig.local.toml
11
+ .DS_Store
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+ *.pem
16
+ *.key
@@ -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,3 @@
1
+ """stdio bridge to msclabs.html-creator, hosted by the MSC Labs CoE."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,5 @@
1
+ """`python -m msclabs_html_creator_mcp_server`, for a client that would rather run a module than a script."""
2
+
3
+ from .bridge import main
4
+
5
+ raise SystemExit(main())
@@ -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())