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 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` over
84
- stdio, or `Authorization: Bearer` over SSE) against `JWT_SECRET`. Invalid or
85
- missing tokens drop the connection before any tool runs.
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 -e .");
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,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpfoundry",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Forge production-ready MCP (Model Context Protocol) servers from databases or OpenAPI specs, with an optional ZTAI zero-trust security shield.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -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 secure}},
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 secure}},
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
- async function main(): Promise<void> {
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
- mcp = FastMCP({{json projectName}})
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__":