mcpfoundry 0.1.0 → 0.1.1
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.
- package/README.md +21 -3
- package/dist/cli.js +20 -2
- package/package.json +1 -1
- package/templates/nodejs/.env.example.hbs +8 -0
- package/templates/nodejs/README.md.hbs +27 -3
- package/templates/nodejs/package.json.hbs +4 -2
- package/templates/nodejs/src/index.ts.hbs +52 -7
- package/templates/python/.env.example.hbs +8 -0
- package/templates/python/README.md.hbs +14 -3
- package/templates/python/server.py.hbs +39 -6
package/README.md
CHANGED
|
@@ -56,10 +56,27 @@ mcpfoundry create \
|
|
|
56
56
|
| `--input` | Path to an OpenAPI spec, JSON or YAML (openapi mode) |
|
|
57
57
|
| `--output` | Output directory (required) |
|
|
58
58
|
| `--lang` | `nodejs` (default) or `python` |
|
|
59
|
+
| `--transport` | `stdio` (default) or `http` |
|
|
60
|
+
| `--port` | Port for the `http` transport (default `3000`) |
|
|
59
61
|
| `--secure` | Embed the optional ZTAI Security Shield |
|
|
60
62
|
| `--force` | Overwrite a non-empty output directory |
|
|
61
63
|
| `--dry-run` | Preview the tools that would be generated, then exit (no files written) |
|
|
62
64
|
|
|
65
|
+
### Transports
|
|
66
|
+
|
|
67
|
+
By default the server runs over **stdio** — the standard way clients (Claude
|
|
68
|
+
Desktop, Claude Code, etc.) launch a local MCP server. Pass `--transport http`
|
|
69
|
+
to generate a server that listens on `http://localhost:<port>/mcp` instead
|
|
70
|
+
(Streamable HTTP via Express for Node, FastMCP for Python):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
mcpfoundry create --type openapi --input ./openapi.yaml --output ./srv --transport http --port 3000
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
With `--secure` + `--transport http`, the JWT guard verifies an
|
|
77
|
+
`Authorization: Bearer <token>` header on **every request** (returns `401` on
|
|
78
|
+
failure); with stdio it verifies `ZTAI_AUTH_TOKEN` once at startup.
|
|
79
|
+
|
|
63
80
|
`--input` accepts a local path **or a URL**, in JSON or YAML.
|
|
64
81
|
|
|
65
82
|
### Preview before generating
|
|
@@ -80,9 +97,10 @@ mcpfoundry create --type openapi --input ./openapi.yaml --dry-run
|
|
|
80
97
|
|
|
81
98
|
When you pass `--secure`, every generated server additionally enforces:
|
|
82
99
|
|
|
83
|
-
1. **JWT Guard** — verifies a short-lived HS256 token (`ZTAI_AUTH_TOKEN`
|
|
84
|
-
stdio, or `Authorization: Bearer`
|
|
85
|
-
missing tokens
|
|
100
|
+
1. **JWT Guard** — verifies a short-lived HS256 token (`ZTAI_AUTH_TOKEN` at
|
|
101
|
+
startup over stdio, or an `Authorization: Bearer` header per request over
|
|
102
|
+
HTTP) against `JWT_SECRET`. Invalid or missing tokens are rejected before any
|
|
103
|
+
tool runs.
|
|
86
104
|
2. **Parameter hardening** — strict Zod/Pydantic schemas (this is on even
|
|
87
105
|
*without* `--secure`, because it's just good hygiene).
|
|
88
106
|
3. **Deception Canary** — when `ZTAI_CANARY_ID` is set, tool output carries a
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,8 @@ program
|
|
|
25
25
|
.option("--input <path>", "OpenAPI/Swagger spec — file path OR URL, JSON or YAML")
|
|
26
26
|
.option("--output <dir>", "output directory for the generated server")
|
|
27
27
|
.option("--lang <lang>", "target language: nodejs | python", "nodejs")
|
|
28
|
+
.option("--transport <transport>", "transport: stdio | http", "stdio")
|
|
29
|
+
.option("--port <port>", "port for the http transport", "3000")
|
|
28
30
|
.option("--secure", "embed the optional ZTAI Security Shield (JWT guard + deception canary)", false)
|
|
29
31
|
.option("--force", "overwrite a non-empty output directory", false)
|
|
30
32
|
.option("--dry-run", "preview the tools that would be generated, then exit", false)
|
|
@@ -34,6 +36,7 @@ Examples:
|
|
|
34
36
|
$ mcpfoundry create --type openapi --input https://petstore3.swagger.io/api/v3/openapi.json --output ./petstore
|
|
35
37
|
$ mcpfoundry create --type database --provider postgres --uri "$DATABASE_URL" --output ./db-server --lang python
|
|
36
38
|
$ mcpfoundry create --type openapi --input ./api.json --output ./secure-server --secure
|
|
39
|
+
$ mcpfoundry create --type openapi --input ./api.json --output ./http-server --transport http --port 3000
|
|
37
40
|
$ mcpfoundry create --type openapi --input ./api.json --output /tmp/x --dry-run
|
|
38
41
|
`)
|
|
39
42
|
.action(async (opts) => {
|
|
@@ -50,6 +53,14 @@ async function run(opts) {
|
|
|
50
53
|
if (lang !== "nodejs" && lang !== "python") {
|
|
51
54
|
throw new Error(`Unsupported --lang "${opts.lang}". Use nodejs or python.`);
|
|
52
55
|
}
|
|
56
|
+
const transport = opts.transport;
|
|
57
|
+
if (transport !== "stdio" && transport !== "http") {
|
|
58
|
+
throw new Error(`Unsupported --transport "${opts.transport}". Use stdio or http.`);
|
|
59
|
+
}
|
|
60
|
+
const port = Number(opts.port);
|
|
61
|
+
if (transport === "http" && (!Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
62
|
+
throw new Error(`Invalid --port "${opts.port}". Use an integer between 1 and 65535.`);
|
|
63
|
+
}
|
|
53
64
|
if (!opts.dryRun && !opts.output) {
|
|
54
65
|
throw new Error("--output is required (or use --dry-run to preview).");
|
|
55
66
|
}
|
|
@@ -98,6 +109,9 @@ async function run(opts) {
|
|
|
98
109
|
sourceType,
|
|
99
110
|
secure: Boolean(opts.secure),
|
|
100
111
|
isDatabase: sourceType === "database",
|
|
112
|
+
transport,
|
|
113
|
+
port,
|
|
114
|
+
isHttp: transport === "http",
|
|
101
115
|
};
|
|
102
116
|
logger_1.logger.info(`Compiling ${tools.length} tool(s) into a ${lang} MCP server…`);
|
|
103
117
|
await (0, compiler_1.compile)(context, outputDir, Boolean(opts.force));
|
|
@@ -119,7 +133,7 @@ function printDryRun(tools) {
|
|
|
119
133
|
}
|
|
120
134
|
function printSummary(ctx, outputDir) {
|
|
121
135
|
logger_1.logger.plain();
|
|
122
|
-
logger_1.logger.success(`Forged ${logger_1.logger.bold(ctx.projectName)} — ${ctx.tools.length} MCP tool(s), ${ctx.lang}${ctx.secure ? ", ZTAI Security Shield enabled" : ""}.`);
|
|
136
|
+
logger_1.logger.success(`Forged ${logger_1.logger.bold(ctx.projectName)} — ${ctx.tools.length} MCP tool(s), ${ctx.lang}, ${ctx.transport} transport${ctx.secure ? ", ZTAI Security Shield enabled" : ""}.`);
|
|
123
137
|
logger_1.logger.plain(` ${logger_1.logger.dim(outputDir)}`);
|
|
124
138
|
logger_1.logger.plain();
|
|
125
139
|
logger_1.logger.plain("Next steps:");
|
|
@@ -130,9 +144,13 @@ function printSummary(ctx, outputDir) {
|
|
|
130
144
|
}
|
|
131
145
|
else {
|
|
132
146
|
logger_1.logger.plain(` cd ${outputDir}`);
|
|
133
|
-
logger_1.logger.plain(" pip install -
|
|
147
|
+
logger_1.logger.plain(" pip install -r requirements.txt");
|
|
134
148
|
logger_1.logger.plain(" python server.py");
|
|
135
149
|
}
|
|
150
|
+
if (ctx.isHttp) {
|
|
151
|
+
logger_1.logger.plain();
|
|
152
|
+
logger_1.logger.plain(` Serves MCP over HTTP at ${logger_1.logger.bold(`http://localhost:${ctx.port}/mcp`)} (override with PORT env).`);
|
|
153
|
+
}
|
|
136
154
|
if (!ctx.secure) {
|
|
137
155
|
logger_1.logger.plain();
|
|
138
156
|
logger_1.logger.warn("Generated a standard MCP server. For zero-trust auth + a deception canary, re-run with --secure, or front it with the ZTAI firewall.");
|
package/package.json
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
# Environment for {{projectName}} (generated by mcpfoundry)
|
|
2
|
+
{{#if isHttp}}
|
|
3
|
+
# Port for the HTTP transport (overrides the compiled-in default {{port}}).
|
|
4
|
+
PORT={{port}}
|
|
5
|
+
{{/if}}
|
|
2
6
|
{{#if isDatabase}}
|
|
3
7
|
# Connection string used by the generated tool handlers.
|
|
4
8
|
DATABASE_URL=
|
|
@@ -7,8 +11,12 @@ DATABASE_URL=
|
|
|
7
11
|
# --- ZTAI Security Shield ---
|
|
8
12
|
# Shared HMAC secret used to verify the ZTAI auth token (HS256). Change in prod.
|
|
9
13
|
JWT_SECRET=dev-secret-key-change-in-production-12345
|
|
14
|
+
{{#if isHttp}}
|
|
15
|
+
# HTTP transport: clients send `Authorization: Bearer <JWT>` on every request.
|
|
16
|
+
{{else}}
|
|
10
17
|
# Short-lived JWT presented over stdio. Invalid/missing => server refuses to start.
|
|
11
18
|
ZTAI_AUTH_TOKEN=
|
|
19
|
+
{{/if}}
|
|
12
20
|
# Optional deception canary. When set, tool output carries a traceable marker.
|
|
13
21
|
ZTAI_CANARY_ID=
|
|
14
22
|
{{/if}}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An MCP (Model Context Protocol) server generated by [mcpfoundry](https://www.npmjs.com/package/mcpfoundry) from a **{{sourceType}}** source.
|
|
4
4
|
|
|
5
|
-
It exposes **{{tools.length}} tool(s)** over stdio.
|
|
5
|
+
It exposes **{{tools.length}} tool(s)** over {{#if isHttp}}HTTP (streamable){{else}}stdio{{/if}}.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -18,6 +18,31 @@ For development with auto-reload:
|
|
|
18
18
|
npm run dev
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
{{#if isHttp}}
|
|
22
|
+
## Transport: HTTP
|
|
23
|
+
|
|
24
|
+
This server listens on `http://localhost:{{port}}/mcp` (override the port with the
|
|
25
|
+
`PORT` env var). Point an MCP client at that URL, or smoke-test it:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -s http://localhost:{{port}}/mcp \
|
|
29
|
+
-H 'Content-Type: application/json' \
|
|
30
|
+
-H 'Accept: application/json, text/event-stream' \
|
|
31
|
+
{{#if secure}} -H "Authorization: Bearer $ZTAI_AUTH_TOKEN" \
|
|
32
|
+
{{/if}} -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
33
|
+
```
|
|
34
|
+
{{else}}
|
|
35
|
+
## Transport: stdio
|
|
36
|
+
|
|
37
|
+
This server talks JSON-RPC over stdin/stdout — the standard way clients (Claude
|
|
38
|
+
Desktop, Claude Code, etc.) launch a local MCP server. Configure your client to
|
|
39
|
+
run `node /abs/path/to/dist/index.js`, or test it with the inspector:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
43
|
+
```
|
|
44
|
+
{{/if}}
|
|
45
|
+
|
|
21
46
|
## Tools
|
|
22
47
|
|
|
23
48
|
{{#each tools}}
|
|
@@ -33,8 +58,7 @@ data source.
|
|
|
33
58
|
|
|
34
59
|
This server was generated with `--secure`, so it enforces:
|
|
35
60
|
|
|
36
|
-
1. **JWT Guard** — on startup it verifies a short-lived HS256 token from
|
|
37
|
-
`ZTAI_AUTH_TOKEN` against `JWT_SECRET`. A missing/invalid token aborts boot.
|
|
61
|
+
1. **JWT Guard** — {{#if isHttp}}every request must carry `Authorization: Bearer <HS256 JWT>`, verified against `JWT_SECRET`; failures return `401`.{{else}}on startup it verifies a short-lived HS256 token from `ZTAI_AUTH_TOKEN` against `JWT_SECRET`. A missing/invalid token aborts boot.{{/if}}
|
|
38
62
|
2. **Parameter hardening** — Zod schemas reject malformed input.
|
|
39
63
|
3. **Deception canary** — set `ZTAI_CANARY_ID` to append a traceable marker to
|
|
40
64
|
tool output (see `src/security.ts`).
|
|
@@ -14,14 +14,16 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.4.0",
|
|
17
|
-
"zod": "^3.23.8"{{#if
|
|
17
|
+
"zod": "^3.23.8"{{#if isHttp}},
|
|
18
|
+
"express": "^4.19.2"{{/if}}{{#if secure}},
|
|
18
19
|
"jsonwebtoken": "^9.0.2"{{/if}}{{#if isDatabase}},
|
|
19
20
|
"pg": "^8.11.5"{{/if}}
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"typescript": "^5.5.4",
|
|
23
24
|
"tsx": "^4.16.0",
|
|
24
|
-
"@types/node": "^20.14.0"{{#if
|
|
25
|
+
"@types/node": "^20.14.0"{{#if isHttp}},
|
|
26
|
+
"@types/express": "^4.17.21"{{/if}}{{#if secure}},
|
|
25
27
|
"@types/jsonwebtoken": "^9.0.6"{{/if}}{{#if isDatabase}},
|
|
26
28
|
"@types/pg": "^8.11.6"{{/if}}
|
|
27
29
|
}
|
|
@@ -1,25 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
{{#if isHttp}}
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import express from "express";
|
|
6
|
+
{{else}}
|
|
3
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { tools } from "./tools.js";
|
|
5
|
-
{{#if secure}}import { verifyStartupToken } from "./security.js";
|
|
6
8
|
{{/if}}
|
|
7
|
-
|
|
8
|
-
{{#if secure}}
|
|
9
|
-
// ZTAI Security Shield: verify the short-lived auth token before serving tools.
|
|
10
|
-
verifyStartupToken();
|
|
9
|
+
import { tools } from "./tools.js";
|
|
10
|
+
{{#if secure}}import { {{#if isHttp}}verifyAuthHeader{{else}}verifyStartupToken{{/if}} } from "./security.js";
|
|
11
11
|
{{/if}}
|
|
12
|
+
function createServer(): McpServer {
|
|
12
13
|
const server = new McpServer({ name: {{json projectName}}, version: "0.1.0" });
|
|
13
|
-
|
|
14
14
|
for (const tool of tools) {
|
|
15
15
|
server.tool(tool.name, tool.description, tool.schema, tool.handler);
|
|
16
16
|
}
|
|
17
|
+
return server;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main(): Promise<void> {
|
|
21
|
+
{{#if isHttp}}
|
|
22
|
+
const app = express();
|
|
23
|
+
app.use(express.json());
|
|
17
24
|
|
|
25
|
+
app.post("/mcp", async (req, res) => {
|
|
26
|
+
{{#if secure}}
|
|
27
|
+
// ZTAI Security Shield: verify the Bearer token on every request.
|
|
28
|
+
try {
|
|
29
|
+
verifyAuthHeader(req.headers.authorization);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
res.status(401).json({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
error: { code: -32001, message: (err as Error).message },
|
|
34
|
+
id: null,
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
{{/if}}
|
|
39
|
+
// Stateless: a fresh server + transport per request.
|
|
40
|
+
const server = createServer();
|
|
41
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
42
|
+
res.on("close", () => {
|
|
43
|
+
void transport.close();
|
|
44
|
+
void server.close();
|
|
45
|
+
});
|
|
46
|
+
await server.connect(transport);
|
|
47
|
+
await transport.handleRequest(req, res, req.body);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const port = Number(process.env.PORT ?? {{port}});
|
|
51
|
+
app.listen(port, () => {
|
|
52
|
+
console.error(
|
|
53
|
+
`[{{projectName}}] MCP server listening on http://localhost:${port}/mcp with ${tools.length} tool(s).`,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
{{else}}
|
|
57
|
+
{{#if secure}}
|
|
58
|
+
// ZTAI Security Shield: verify the short-lived auth token before serving tools.
|
|
59
|
+
verifyStartupToken();
|
|
60
|
+
{{/if}}
|
|
61
|
+
const server = createServer();
|
|
18
62
|
const transport = new StdioServerTransport();
|
|
19
63
|
await server.connect(transport);
|
|
20
64
|
console.error(
|
|
21
65
|
`[{{projectName}}] MCP server running over stdio with ${tools.length} tool(s).`,
|
|
22
66
|
);
|
|
67
|
+
{{/if}}
|
|
23
68
|
}
|
|
24
69
|
|
|
25
70
|
main().catch((err) => {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
# Environment for {{projectName}} (generated by mcpfoundry)
|
|
2
|
+
{{#if isHttp}}
|
|
3
|
+
# Port for the HTTP transport (compiled-in default is {{port}}).
|
|
4
|
+
PORT={{port}}
|
|
5
|
+
{{/if}}
|
|
2
6
|
{{#if isDatabase}}
|
|
3
7
|
# Connection string used by the generated tool handlers.
|
|
4
8
|
DATABASE_URL=
|
|
@@ -7,8 +11,12 @@ DATABASE_URL=
|
|
|
7
11
|
# --- ZTAI Security Shield ---
|
|
8
12
|
# Shared HMAC secret used to verify the ZTAI auth token (HS256). Change in prod.
|
|
9
13
|
JWT_SECRET=dev-secret-key-change-in-production-12345
|
|
14
|
+
{{#if isHttp}}
|
|
15
|
+
# HTTP transport: clients send `Authorization: Bearer <JWT>` on every request.
|
|
16
|
+
{{else}}
|
|
10
17
|
# Short-lived JWT presented over stdio. Invalid/missing => server refuses to start.
|
|
11
18
|
ZTAI_AUTH_TOKEN=
|
|
19
|
+
{{/if}}
|
|
12
20
|
# Optional deception canary. When set, tool output carries a traceable marker.
|
|
13
21
|
ZTAI_CANARY_ID=
|
|
14
22
|
{{/if}}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An MCP (Model Context Protocol) server generated by [mcpfoundry](https://www.npmjs.com/package/mcpfoundry) from a **{{sourceType}}** source, built on [FastMCP](https://github.com/jlowin/fastmcp).
|
|
4
4
|
|
|
5
|
-
It exposes **{{tools.length}} tool(s)** over stdio.
|
|
5
|
+
It exposes **{{tools.length}} tool(s)** over {{#if isHttp}}HTTP (streamable){{else}}stdio{{/if}}.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -17,6 +17,18 @@ python server.py
|
|
|
17
17
|
> Prefer packaging? `pip install -e .` also works (uses `pyproject.toml`), but
|
|
18
18
|
> requires a recent `pip`/`setuptools`.
|
|
19
19
|
|
|
20
|
+
{{#if isHttp}}
|
|
21
|
+
## Transport: HTTP
|
|
22
|
+
|
|
23
|
+
This server listens on `http://localhost:{{port}}/mcp` (override with the `PORT`
|
|
24
|
+
env var). Point an MCP client at that URL.
|
|
25
|
+
{{else}}
|
|
26
|
+
## Transport: stdio
|
|
27
|
+
|
|
28
|
+
This server talks JSON-RPC over stdin/stdout. Configure your MCP client to run
|
|
29
|
+
`python /abs/path/to/server.py`.
|
|
30
|
+
{{/if}}
|
|
31
|
+
|
|
20
32
|
## Tools
|
|
21
33
|
|
|
22
34
|
{{#each tools}}
|
|
@@ -32,8 +44,7 @@ data source.
|
|
|
32
44
|
|
|
33
45
|
This server was generated with `--secure`, so it enforces:
|
|
34
46
|
|
|
35
|
-
1. **JWT Guard** — on startup it verifies a short-lived HS256 token from
|
|
36
|
-
`ZTAI_AUTH_TOKEN` against `JWT_SECRET`. A missing/invalid token aborts boot.
|
|
47
|
+
1. **JWT Guard** — {{#if isHttp}}every request must carry `Authorization: Bearer <HS256 JWT>`, verified against `JWT_SECRET` by a FastMCP middleware.{{else}}on startup it verifies a short-lived HS256 token from `ZTAI_AUTH_TOKEN` against `JWT_SECRET`. A missing/invalid token aborts boot.{{/if}}
|
|
37
48
|
2. **Parameter hardening** — Pydantic models reject malformed input.
|
|
38
49
|
3. **Deception canary** — set `ZTAI_CANARY_ID` to append a traceable marker to
|
|
39
50
|
tool output (see `apply_canary` in `server.py`).
|
|
@@ -13,11 +13,35 @@ import jwt
|
|
|
13
13
|
{{/if}}
|
|
14
14
|
from fastmcp import FastMCP
|
|
15
15
|
from pydantic import Field
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
{{#if secure}}{{#if isHttp}}
|
|
17
|
+
from fastmcp.server.dependencies import get_http_request
|
|
18
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
19
|
+
{{/if}}{{/if}}
|
|
18
20
|
{{#if secure}}
|
|
19
|
-
|
|
20
21
|
# --- ZTAI Security Shield (opt-in) ---
|
|
22
|
+
{{#if isHttp}}
|
|
23
|
+
|
|
24
|
+
def verify_auth_header(authorization: str | None) -> dict[str, Any]:
|
|
25
|
+
"""Verify a Bearer JWT (HS256) from the Authorization header (per request)."""
|
|
26
|
+
secret = os.environ.get("JWT_SECRET")
|
|
27
|
+
if not secret:
|
|
28
|
+
raise RuntimeError("ZTAI Shield: JWT_SECRET is not set.")
|
|
29
|
+
if not authorization or not authorization.lower().startswith("bearer "):
|
|
30
|
+
raise RuntimeError("ZTAI Shield: missing Bearer token.")
|
|
31
|
+
token = authorization.split(" ", 1)[1]
|
|
32
|
+
try:
|
|
33
|
+
return jwt.decode(token, secret, algorithms=["HS256"])
|
|
34
|
+
except jwt.PyJWTError as exc:
|
|
35
|
+
raise RuntimeError(f"ZTAI Shield: invalid token ({exc}). Connection dropped.") from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ZtaiAuthMiddleware(Middleware):
|
|
39
|
+
"""Reject any request whose Authorization header fails JWT verification."""
|
|
40
|
+
|
|
41
|
+
async def on_request(self, context: MiddlewareContext, call_next):
|
|
42
|
+
verify_auth_header(get_http_request().headers.get("authorization"))
|
|
43
|
+
return await call_next(context)
|
|
44
|
+
{{else}}
|
|
21
45
|
|
|
22
46
|
def verify_startup_token() -> dict[str, Any]:
|
|
23
47
|
"""Verify the short-lived HS256 token before serving any tools.
|
|
@@ -37,7 +61,7 @@ def verify_startup_token() -> dict[str, Any]:
|
|
|
37
61
|
raise RuntimeError(
|
|
38
62
|
f"ZTAI Shield: invalid ZTAI_AUTH_TOKEN ({exc}). Connection dropped."
|
|
39
63
|
) from exc
|
|
40
|
-
|
|
64
|
+
{{/if}}
|
|
41
65
|
|
|
42
66
|
def apply_canary(text: str) -> str:
|
|
43
67
|
"""Append a traceable deception marker when ZTAI_CANARY_ID is set."""
|
|
@@ -48,6 +72,11 @@ def apply_canary(text: str) -> str:
|
|
|
48
72
|
return f"{text}\n<!-- ztai-canary-{seed} -->"
|
|
49
73
|
{{/if}}
|
|
50
74
|
|
|
75
|
+
mcp = FastMCP({{json projectName}})
|
|
76
|
+
{{#if secure}}{{#if isHttp}}
|
|
77
|
+
mcp.add_middleware(ZtaiAuthMiddleware())
|
|
78
|
+
{{/if}}{{/if}}
|
|
79
|
+
|
|
51
80
|
{{#each tools}}
|
|
52
81
|
@mcp.tool()
|
|
53
82
|
def {{this.name}}(
|
|
@@ -64,10 +93,14 @@ def {{this.name}}(
|
|
|
64
93
|
|
|
65
94
|
{{/each}}
|
|
66
95
|
def main() -> None:
|
|
67
|
-
{{#if secure}}
|
|
96
|
+
{{#if secure}}{{#unless isHttp}}
|
|
68
97
|
verify_startup_token()
|
|
69
|
-
{{/if}}
|
|
98
|
+
{{/unless}}{{/if}}
|
|
99
|
+
{{#if isHttp}}
|
|
100
|
+
mcp.run(transport="http", port=int(os.environ.get("PORT", {{port}})))
|
|
101
|
+
{{else}}
|
|
70
102
|
mcp.run()
|
|
103
|
+
{{/if}}
|
|
71
104
|
|
|
72
105
|
|
|
73
106
|
if __name__ == "__main__":
|