mcpfoundry 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mcpfoundry contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # mcpfoundry
2
+
3
+ > Forge production-ready **MCP (Model Context Protocol) servers** from your existing data — a database or an OpenAPI spec — in seconds.
4
+
5
+ `mcpfoundry` is a zero-friction CLI that introspects a data source and scaffolds a
6
+ clean, self-contained, runnable MCP server. Generated servers always ship with
7
+ **parameter validation** (Zod / Pydantic). An **optional** zero-trust
8
+ **ZTAI Security Shield** (JWT guard + deception canary) can be layered in with a
9
+ single `--secure` flag — recommended, never forced.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g mcpfoundry
15
+ # or run ad-hoc:
16
+ npx mcpfoundry create --help
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### From an OpenAPI / Swagger spec
22
+
23
+ Every endpoint becomes a typed MCP tool:
24
+
25
+ ```bash
26
+ mcpfoundry create \
27
+ --type openapi \
28
+ --input ./openapi.json \
29
+ --output ./my-mcp-server \
30
+ --lang nodejs
31
+ ```
32
+
33
+ ### From a database
34
+
35
+ Tables are introspected and turned into CRUD tools:
36
+
37
+ ```bash
38
+ mcpfoundry create \
39
+ --type database \
40
+ --provider postgres \
41
+ --uri "postgresql://user:pass@localhost:5432/mydb" \
42
+ --output ./my-mcp-server \
43
+ --lang python
44
+ ```
45
+
46
+ > Postgres is fully supported today. MySQL and MongoDB are stubbed and open for
47
+ > [contributions](./CONTRIBUTING.md).
48
+
49
+ ### Flags
50
+
51
+ | Flag | Description |
52
+ | --- | --- |
53
+ | `--type` | `database` or `openapi` (required) |
54
+ | `--provider` | `postgres` \| `mysql` \| `mongodb` (database mode) |
55
+ | `--uri` | DB connection string (database mode) |
56
+ | `--input` | Path to an OpenAPI spec, JSON or YAML (openapi mode) |
57
+ | `--output` | Output directory (required) |
58
+ | `--lang` | `nodejs` (default) or `python` |
59
+ | `--secure` | Embed the optional ZTAI Security Shield |
60
+ | `--force` | Overwrite a non-empty output directory |
61
+ | `--dry-run` | Preview the tools that would be generated, then exit (no files written) |
62
+
63
+ `--input` accepts a local path **or a URL**, in JSON or YAML.
64
+
65
+ ### Preview before generating
66
+
67
+ ```bash
68
+ mcpfoundry create --type openapi --input ./openapi.yaml --dry-run
69
+ ```
70
+
71
+ ```
72
+ ✔ Dry run — 4 tool(s) would be generated:
73
+ • list_pets(limit?: integer)
74
+ • create_pet(name: string, tag?: string)
75
+ • get_pet_by_id(pet_id: integer)
76
+ • delete_pet(pet_id: integer)
77
+ ```
78
+
79
+ ## The optional ZTAI Security Shield (`--secure`)
80
+
81
+ When you pass `--secure`, every generated server additionally enforces:
82
+
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.
86
+ 2. **Parameter hardening** — strict Zod/Pydantic schemas (this is on even
87
+ *without* `--secure`, because it's just good hygiene).
88
+ 3. **Deception Canary** — when `ZTAI_CANARY_ID` is set, tool output carries a
89
+ subtle, traceable marker to help detect adversarial exfiltration.
90
+
91
+ Without `--secure` you still get a perfectly good, vendor-neutral MCP server.
92
+
93
+ ## Architecture — the Template-Compiler pattern
94
+
95
+ ```
96
+ src/
97
+ cli.ts # arg parsing + orchestration
98
+ parsers/ # data source -> normalized IR (ToolSpec[])
99
+ compiler.ts # IR + Handlebars templates -> generated project
100
+ types.ts # the shared IR
101
+ templates/
102
+ nodejs/ # @modelcontextprotocol/sdk + Zod
103
+ python/ # FastMCP + Pydantic
104
+ ```
105
+
106
+ Parsers and templates are decoupled by a normalized intermediate representation,
107
+ so **adding a new language is just a new `templates/<lang>/` folder** — no engine
108
+ changes. See [CONTRIBUTING.md](./CONTRIBUTING.md).
109
+
110
+ ## Develop
111
+
112
+ ```bash
113
+ npm install
114
+ npm run build
115
+ node dist/cli.js create --type openapi --input examples/petstore.openapi.json --output /tmp/petstore-mcp
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const commander_1 = require("commander");
9
+ const database_1 = require("./parsers/database");
10
+ const openapi_1 = require("./parsers/openapi");
11
+ const compiler_1 = require("./compiler");
12
+ const logger_1 = require("./utils/logger");
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name("mcpfoundry")
16
+ .description("Forge production-ready MCP servers from databases or OpenAPI specs.")
17
+ .version("0.1.0")
18
+ .showHelpAfterError("(add --help for usage)");
19
+ program
20
+ .command("create")
21
+ .description("Generate an MCP server from a data source.")
22
+ .requiredOption("--type <type>", "source type: database | openapi")
23
+ .option("--provider <provider>", "database provider: postgres | mysql | mongodb")
24
+ .option("--uri <uri>", "database connection string (for --type database)")
25
+ .option("--input <path>", "OpenAPI/Swagger spec — file path OR URL, JSON or YAML")
26
+ .option("--output <dir>", "output directory for the generated server")
27
+ .option("--lang <lang>", "target language: nodejs | python", "nodejs")
28
+ .option("--secure", "embed the optional ZTAI Security Shield (JWT guard + deception canary)", false)
29
+ .option("--force", "overwrite a non-empty output directory", false)
30
+ .option("--dry-run", "preview the tools that would be generated, then exit", false)
31
+ .addHelpText("after", `
32
+ Examples:
33
+ $ mcpfoundry create --type openapi --input ./openapi.yaml --output ./my-server
34
+ $ mcpfoundry create --type openapi --input https://petstore3.swagger.io/api/v3/openapi.json --output ./petstore
35
+ $ mcpfoundry create --type database --provider postgres --uri "$DATABASE_URL" --output ./db-server --lang python
36
+ $ mcpfoundry create --type openapi --input ./api.json --output ./secure-server --secure
37
+ $ mcpfoundry create --type openapi --input ./api.json --output /tmp/x --dry-run
38
+ `)
39
+ .action(async (opts) => {
40
+ try {
41
+ await run(opts);
42
+ }
43
+ catch (err) {
44
+ logger_1.logger.error(err.message);
45
+ process.exit(1);
46
+ }
47
+ });
48
+ async function run(opts) {
49
+ const lang = opts.lang;
50
+ if (lang !== "nodejs" && lang !== "python") {
51
+ throw new Error(`Unsupported --lang "${opts.lang}". Use nodejs or python.`);
52
+ }
53
+ if (!opts.dryRun && !opts.output) {
54
+ throw new Error("--output is required (or use --dry-run to preview).");
55
+ }
56
+ let tools;
57
+ let sourceType;
58
+ if (opts.type === "database") {
59
+ if (!opts.provider)
60
+ throw new Error("--provider is required for --type database.");
61
+ if (!opts.uri)
62
+ throw new Error("--uri is required for --type database.");
63
+ sourceType = "database";
64
+ logger_1.logger.info(`Introspecting ${opts.provider} database…`);
65
+ tools = await (0, database_1.introspectDatabase)(opts.provider, opts.uri);
66
+ }
67
+ else if (opts.type === "openapi") {
68
+ if (!opts.input)
69
+ throw new Error("--input is required for --type openapi.");
70
+ sourceType = "openapi";
71
+ logger_1.logger.info(`Parsing OpenAPI spec at ${opts.input}…`);
72
+ tools = await (0, openapi_1.parseOpenApi)(opts.input);
73
+ }
74
+ else {
75
+ throw new Error(`Unsupported --type "${opts.type ?? ""}". Use database or openapi.`);
76
+ }
77
+ if (tools.length === 0) {
78
+ throw new Error("No tools were generated from the source — nothing to scaffold.");
79
+ }
80
+ // Required params first: Python disallows a non-default arg after a defaulted
81
+ // one, and it reads better everywhere else too. Stable within each group.
82
+ for (const tool of tools) {
83
+ tool.params = [
84
+ ...tool.params.filter((p) => p.required),
85
+ ...tool.params.filter((p) => !p.required),
86
+ ];
87
+ }
88
+ if (opts.dryRun) {
89
+ printDryRun(tools);
90
+ return;
91
+ }
92
+ const outputDir = node_path_1.default.resolve(opts.output);
93
+ const projectName = node_path_1.default.basename(outputDir);
94
+ const context = {
95
+ projectName,
96
+ tools,
97
+ lang,
98
+ sourceType,
99
+ secure: Boolean(opts.secure),
100
+ isDatabase: sourceType === "database",
101
+ };
102
+ logger_1.logger.info(`Compiling ${tools.length} tool(s) into a ${lang} MCP server…`);
103
+ await (0, compiler_1.compile)(context, outputDir, Boolean(opts.force));
104
+ printSummary(context, outputDir);
105
+ }
106
+ function printDryRun(tools) {
107
+ logger_1.logger.plain();
108
+ logger_1.logger.success(`Dry run — ${tools.length} tool(s) would be generated:`);
109
+ logger_1.logger.plain();
110
+ for (const t of tools) {
111
+ const params = t.params
112
+ .map((p) => `${p.name}${p.required ? "" : "?"}: ${p.type}`)
113
+ .join(", ");
114
+ logger_1.logger.plain(` • ${logger_1.logger.bold(t.name)}(${params})`);
115
+ logger_1.logger.plain(` ${logger_1.logger.dim(t.description)}`);
116
+ }
117
+ logger_1.logger.plain();
118
+ logger_1.logger.plain(logger_1.logger.dim("No files written. Drop --dry-run to generate."));
119
+ }
120
+ function printSummary(ctx, outputDir) {
121
+ 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" : ""}.`);
123
+ logger_1.logger.plain(` ${logger_1.logger.dim(outputDir)}`);
124
+ logger_1.logger.plain();
125
+ logger_1.logger.plain("Next steps:");
126
+ if (ctx.lang === "nodejs") {
127
+ logger_1.logger.plain(` cd ${outputDir}`);
128
+ logger_1.logger.plain(" npm install");
129
+ logger_1.logger.plain(" npm run build && npm start");
130
+ }
131
+ else {
132
+ logger_1.logger.plain(` cd ${outputDir}`);
133
+ logger_1.logger.plain(" pip install -e .");
134
+ logger_1.logger.plain(" python server.py");
135
+ }
136
+ if (!ctx.secure) {
137
+ logger_1.logger.plain();
138
+ 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.");
139
+ }
140
+ }
141
+ program.parseAsync(process.argv);
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compile = compile;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const handlebars_1 = __importDefault(require("handlebars"));
10
+ const naming_1 = require("./utils/naming");
11
+ /** Templates ship alongside the compiled engine: dist/compiler.js -> ../templates. */
12
+ const TEMPLATES_ROOT = node_path_1.default.join(__dirname, "..", "templates");
13
+ let helpersRegistered = false;
14
+ function registerHelpers() {
15
+ if (helpersRegistered)
16
+ return;
17
+ // Emit a JSON-encoded literal (safe string keys, escaped values).
18
+ handlebars_1.default.registerHelper("json", (value) => new handlebars_1.default.SafeString(JSON.stringify(value ?? null)));
19
+ handlebars_1.default.registerHelper("pascalCase", (s) => (0, naming_1.pascalCase)(String(s ?? "")));
20
+ handlebars_1.default.registerHelper("camelCase", (s) => (0, naming_1.camelCase)(String(s ?? "")));
21
+ handlebars_1.default.registerHelper("eq", (a, b) => a === b);
22
+ // Language-specific type mappers used inside the templates.
23
+ handlebars_1.default.registerHelper("zodType", (p) => new handlebars_1.default.SafeString(zodType(p)));
24
+ handlebars_1.default.registerHelper("pydanticType", (p) => new handlebars_1.default.SafeString(pydanticType(p)));
25
+ // Full Python function parameter, e.g.
26
+ // name: Annotated[str, Field(description="...")]
27
+ // tag: str | None = None
28
+ handlebars_1.default.registerHelper("pyParam", (p) => new handlebars_1.default.SafeString(pyParam(p)));
29
+ // Python dict literal of the call args, e.g. {"name": name, "tag": tag}.
30
+ handlebars_1.default.registerHelper("pyArgsDict", (params) => {
31
+ const entries = (params ?? [])
32
+ .map((p) => `${JSON.stringify(p.name)}: ${p.name}`)
33
+ .join(", ");
34
+ return new handlebars_1.default.SafeString(`{${entries}}`);
35
+ });
36
+ helpersRegistered = true;
37
+ }
38
+ /** Map an IR ParamSpec to a Zod expression for Node.js parameter hardening. */
39
+ function zodType(p) {
40
+ const base = {
41
+ string: "z.string()",
42
+ number: "z.number()",
43
+ integer: "z.number().int()",
44
+ boolean: "z.boolean()",
45
+ object: "z.record(z.any())",
46
+ array: "z.array(z.any())",
47
+ };
48
+ let expr = base[p.type] ?? "z.any()";
49
+ if (p.description)
50
+ expr += `.describe(${JSON.stringify(p.description)})`;
51
+ if (!p.required)
52
+ expr += ".optional()";
53
+ return expr;
54
+ }
55
+ /** Map an IR ParamSpec to a Python type annotation for Pydantic hardening. */
56
+ function pydanticType(p) {
57
+ const base = {
58
+ string: "str",
59
+ number: "float",
60
+ integer: "int",
61
+ boolean: "bool",
62
+ object: "dict[str, Any]",
63
+ array: "list[Any]",
64
+ };
65
+ const t = base[p.type] ?? "Any";
66
+ return p.required ? t : `${t} | None`;
67
+ }
68
+ /** Render a full Python function parameter (type + optional Field + default). */
69
+ function pyParam(p) {
70
+ const type = pydanticType(p);
71
+ const annotated = p.description
72
+ ? `Annotated[${type}, Field(description=${JSON.stringify(p.description)})]`
73
+ : type;
74
+ return p.required ? `${p.name}: ${annotated}` : `${p.name}: ${annotated} = None`;
75
+ }
76
+ function walk(dir) {
77
+ const out = [];
78
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
79
+ const full = node_path_1.default.join(dir, entry.name);
80
+ if (entry.isDirectory())
81
+ out.push(...walk(full));
82
+ else
83
+ out.push(full);
84
+ }
85
+ return out;
86
+ }
87
+ /**
88
+ * Render the selected language template against the compile context and write
89
+ * the result to the output directory. The engine is fully data-driven: adding a
90
+ * new language means adding a `templates/<lang>/` folder, no engine changes.
91
+ */
92
+ async function compile(ctx, outputDir, force) {
93
+ registerHelpers();
94
+ const templateDir = node_path_1.default.join(TEMPLATES_ROOT, ctx.lang);
95
+ if (!node_fs_1.default.existsSync(templateDir)) {
96
+ throw new Error(`No templates found for lang "${ctx.lang}" (looked in ${templateDir}).`);
97
+ }
98
+ if (node_fs_1.default.existsSync(outputDir) &&
99
+ node_fs_1.default.readdirSync(outputDir).length > 0 &&
100
+ !force) {
101
+ throw new Error(`Output directory ${outputDir} is not empty. Re-run with --force to overwrite.`);
102
+ }
103
+ node_fs_1.default.mkdirSync(outputDir, { recursive: true });
104
+ for (const abs of walk(templateDir)) {
105
+ const rel = node_path_1.default.relative(templateDir, abs);
106
+ const isTemplate = rel.endsWith(".hbs");
107
+ const outRel = isTemplate ? rel.slice(0, -".hbs".length) : rel;
108
+ // Dotfile convention: npm strips files literally named `.gitignore` from
109
+ // published packages, so they are stored as `_gitignore` and the leading
110
+ // underscore is restored to a dot on output.
111
+ const dir = node_path_1.default.dirname(outRel);
112
+ const base = node_path_1.default.basename(outRel).replace(/^_/, ".");
113
+ const outPath = node_path_1.default.join(outputDir, dir, base);
114
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(outPath), { recursive: true });
115
+ const raw = node_fs_1.default.readFileSync(abs, "utf8");
116
+ const content = isTemplate
117
+ ? handlebars_1.default.compile(raw, { noEscape: true })(ctx)
118
+ : raw;
119
+ // A template that renders to nothing (e.g. a fully `{{#if secure}}`-gated
120
+ // file in a non-secure build) is intentionally omitted from the output.
121
+ if (isTemplate && content.trim() === "")
122
+ continue;
123
+ node_fs_1.default.writeFileSync(outPath, content);
124
+ }
125
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.introspectDatabase = introspectDatabase;
4
+ const pg_1 = require("pg");
5
+ const naming_1 = require("../utils/naming");
6
+ const NOT_IMPLEMENTED = "introspection is not yet implemented — contributions welcome! See CONTRIBUTING.md to add a provider.";
7
+ /**
8
+ * Connect to a database, introspect its schema, and emit CRUD MCP tools.
9
+ * Only Postgres is implemented today; MySQL/MongoDB are intentionally stubbed
10
+ * with a friendly pointer so the community can extend them.
11
+ */
12
+ async function introspectDatabase(provider, uri) {
13
+ switch (provider) {
14
+ case "postgres":
15
+ return introspectPostgres(uri);
16
+ case "mysql":
17
+ throw new Error(`MySQL ${NOT_IMPLEMENTED}`);
18
+ case "mongodb":
19
+ throw new Error(`MongoDB ${NOT_IMPLEMENTED}`);
20
+ default:
21
+ throw new Error(`Unknown provider "${provider}". Supported: postgres (mysql, mongodb coming soon).`);
22
+ }
23
+ }
24
+ async function introspectPostgres(uri) {
25
+ const client = new pg_1.Client({ connectionString: uri });
26
+ try {
27
+ await client.connect();
28
+ }
29
+ catch (err) {
30
+ throw new Error(`Could not connect to Postgres (${err.message}). ` +
31
+ `Check the --uri host/port/credentials and that the database is reachable.`);
32
+ }
33
+ try {
34
+ const columnsRes = await client.query(`SELECT table_name, column_name, data_type, is_nullable
35
+ FROM information_schema.columns
36
+ WHERE table_schema = 'public'
37
+ ORDER BY table_name, ordinal_position`);
38
+ const pkRes = await client.query(`SELECT kcu.table_name, kcu.column_name
39
+ FROM information_schema.table_constraints tc
40
+ JOIN information_schema.key_column_usage kcu
41
+ ON tc.constraint_name = kcu.constraint_name
42
+ AND tc.table_schema = kcu.table_schema
43
+ WHERE tc.constraint_type = 'PRIMARY KEY'
44
+ AND tc.table_schema = 'public'`);
45
+ const primaryKeys = new Map();
46
+ for (const row of pkRes.rows) {
47
+ if (!primaryKeys.has(row.table_name)) {
48
+ primaryKeys.set(row.table_name, new Set());
49
+ }
50
+ primaryKeys.get(row.table_name).add(row.column_name);
51
+ }
52
+ const tables = new Map();
53
+ for (const row of columnsRes.rows) {
54
+ const pk = primaryKeys.get(row.table_name);
55
+ const col = {
56
+ name: (0, naming_1.sanitizeIdentifier)(row.column_name),
57
+ type: mapPgType(row.data_type),
58
+ nullable: row.is_nullable === "YES",
59
+ isPrimaryKey: pk?.has(row.column_name) ?? false,
60
+ };
61
+ if (!tables.has(row.table_name))
62
+ tables.set(row.table_name, []);
63
+ tables.get(row.table_name).push(col);
64
+ }
65
+ const tools = [];
66
+ for (const [table, columns] of tables) {
67
+ tools.push(...buildCrudTools(table, columns));
68
+ }
69
+ return tools;
70
+ }
71
+ finally {
72
+ await client.end();
73
+ }
74
+ }
75
+ function buildCrudTools(table, columns) {
76
+ const safeTable = (0, naming_1.sanitizeIdentifier)(table);
77
+ const keyCols = columns.filter((c) => c.isPrimaryKey);
78
+ const nonKeyCols = columns.filter((c) => !c.isPrimaryKey);
79
+ const keyParams = keyCols.map((c) => ({
80
+ name: c.name,
81
+ type: c.type,
82
+ required: true,
83
+ description: (0, naming_1.cleanDescription)(`Primary key (${c.name}) of ${table}`),
84
+ }));
85
+ const tools = [];
86
+ // list_<table>: always available.
87
+ tools.push({
88
+ name: `list_${safeTable}`,
89
+ description: `List rows from the ${table} table.`,
90
+ params: [
91
+ { name: "limit", type: "integer", required: false, description: "Max rows to return." },
92
+ { name: "offset", type: "integer", required: false, description: "Rows to skip." },
93
+ ],
94
+ source: "database",
95
+ operation: "list",
96
+ table,
97
+ });
98
+ // create_<table>: always available.
99
+ tools.push({
100
+ name: `create_${safeTable}`,
101
+ description: `Insert a new row into the ${table} table.`,
102
+ params: nonKeyCols.map((c) => ({
103
+ name: c.name,
104
+ type: c.type,
105
+ required: !c.nullable,
106
+ })),
107
+ source: "database",
108
+ operation: "create",
109
+ table,
110
+ });
111
+ // get/update/delete need a primary key to address a single row.
112
+ if (keyCols.length > 0) {
113
+ tools.push({
114
+ name: `get_${safeTable}`,
115
+ description: `Fetch a single ${table} row by primary key.`,
116
+ params: keyParams,
117
+ source: "database",
118
+ operation: "get",
119
+ table,
120
+ });
121
+ tools.push({
122
+ name: `update_${safeTable}`,
123
+ description: `Update a ${table} row by primary key.`,
124
+ params: [
125
+ ...keyParams,
126
+ ...nonKeyCols.map((c) => ({
127
+ name: c.name,
128
+ type: c.type,
129
+ required: false,
130
+ })),
131
+ ],
132
+ source: "database",
133
+ operation: "update",
134
+ table,
135
+ });
136
+ tools.push({
137
+ name: `delete_${safeTable}`,
138
+ description: `Delete a ${table} row by primary key.`,
139
+ params: keyParams,
140
+ source: "database",
141
+ operation: "delete",
142
+ table,
143
+ });
144
+ }
145
+ return tools;
146
+ }
147
+ function mapPgType(dataType) {
148
+ const t = dataType.toLowerCase();
149
+ if (/(int|serial|bigint|smallint)/.test(t))
150
+ return "integer";
151
+ if (/(numeric|decimal|real|double)/.test(t))
152
+ return "number";
153
+ if (t === "boolean")
154
+ return "boolean";
155
+ if (/json/.test(t))
156
+ return "object";
157
+ if (t.includes("array"))
158
+ return "array";
159
+ return "string";
160
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseOpenApi = parseOpenApi;
7
+ const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
8
+ const naming_1 = require("../utils/naming");
9
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
10
+ /**
11
+ * Ingest an OpenAPI / Swagger document (JSON or YAML; SwaggerParser handles
12
+ * both, plus $ref dereferencing) and map every operation to one MCP tool.
13
+ */
14
+ async function parseOpenApi(input) {
15
+ // dereference resolves all $ref pointers so we can read schemas inline.
16
+ let api;
17
+ try {
18
+ api = await swagger_parser_1.default.dereference(input);
19
+ }
20
+ catch (err) {
21
+ throw new Error(`Could not read OpenAPI spec "${input}" (${err.message}). ` +
22
+ `Check the path/URL is reachable and the document is valid JSON or YAML.`);
23
+ }
24
+ const paths = api.paths ?? {};
25
+ const tools = [];
26
+ for (const [routePath, pathItem] of Object.entries(paths)) {
27
+ for (const method of HTTP_METHODS) {
28
+ const op = pathItem?.[method];
29
+ if (!op)
30
+ continue;
31
+ const name = (0, naming_1.sanitizeIdentifier)(op.operationId || `${method}_${routePath}`);
32
+ const params = [];
33
+ // Path / query / header / cookie parameters.
34
+ for (const prm of op.parameters ?? []) {
35
+ params.push({
36
+ name: (0, naming_1.sanitizeIdentifier)(prm.name),
37
+ type: jsonSchemaType(prm.schema?.type),
38
+ required: Boolean(prm.required),
39
+ description: (0, naming_1.cleanDescription)(prm.description),
40
+ });
41
+ }
42
+ // JSON request body properties.
43
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema ?? undefined;
44
+ if (bodySchema?.properties) {
45
+ const required = new Set(bodySchema.required ?? []);
46
+ for (const [propName, propSchema] of Object.entries(bodySchema.properties)) {
47
+ params.push({
48
+ name: (0, naming_1.sanitizeIdentifier)(propName),
49
+ type: jsonSchemaType(propSchema?.type),
50
+ required: required.has(propName),
51
+ description: (0, naming_1.cleanDescription)(propSchema?.description),
52
+ });
53
+ }
54
+ }
55
+ tools.push({
56
+ name,
57
+ description: (0, naming_1.cleanDescription)(op.summary || op.description) ||
58
+ `${method.toUpperCase()} ${routePath}`,
59
+ params: (0, naming_1.dedupeByName)(params),
60
+ source: "openapi",
61
+ method: method.toUpperCase(),
62
+ path: routePath,
63
+ });
64
+ }
65
+ }
66
+ return tools;
67
+ }
68
+ function jsonSchemaType(type) {
69
+ switch (type) {
70
+ case "integer":
71
+ return "integer";
72
+ case "number":
73
+ return "number";
74
+ case "boolean":
75
+ return "boolean";
76
+ case "array":
77
+ return "array";
78
+ case "object":
79
+ return "object";
80
+ default:
81
+ return "string";
82
+ }
83
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Shared intermediate representation (IR) that every parser emits and the
4
+ * compiler engine consumes. Keeping a normalized IR is what lets the
5
+ * Template-Compiler pattern stay language-agnostic: parsers know nothing about
6
+ * the target language, and templates know nothing about the source.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logger = void 0;
4
+ /**
5
+ * Minimal colored terminal logger. Everything goes to stderr so that stdout
6
+ * stays clean for potential machine consumption / piping.
7
+ */
8
+ const c = {
9
+ reset: "\x1b[0m",
10
+ red: "\x1b[31m",
11
+ green: "\x1b[32m",
12
+ yellow: "\x1b[33m",
13
+ cyan: "\x1b[36m",
14
+ gray: "\x1b[90m",
15
+ bold: "\x1b[1m",
16
+ };
17
+ exports.logger = {
18
+ info: (msg) => console.error(`${c.cyan}›${c.reset} ${msg}`),
19
+ success: (msg) => console.error(`${c.green}✔${c.reset} ${msg}`),
20
+ warn: (msg) => console.error(`${c.yellow}⚠${c.reset} ${msg}`),
21
+ error: (msg) => console.error(`${c.red}✖${c.reset} ${msg}`),
22
+ plain: (msg = "") => console.error(msg),
23
+ bold: (msg) => `${c.bold}${msg}${c.reset}`,
24
+ dim: (msg) => `${c.gray}${msg}${c.reset}`,
25
+ };
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * Identifier sanitization helpers. Source names (table names, OpenAPI paths,
4
+ * operationIds, column names) cannot be trusted to be valid identifiers in the
5
+ * target language, so everything is normalized before it reaches a template.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.sanitizeIdentifier = sanitizeIdentifier;
9
+ exports.pascalCase = pascalCase;
10
+ exports.camelCase = camelCase;
11
+ exports.cleanDescription = cleanDescription;
12
+ exports.dedupeByName = dedupeByName;
13
+ /**
14
+ * Convert any source name into a clean, language-safe `snake_case` identifier.
15
+ * camelCase and ACRONYMBoundaries are split so `getPetById` -> `get_pet_by_id`
16
+ * (conventional MCP tool naming) rather than the ugly `getpetbyid`.
17
+ */
18
+ function sanitizeIdentifier(raw) {
19
+ let s = String(raw)
20
+ .trim()
21
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2") // camelCase boundary
22
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") // ACRONYMWord boundary
23
+ .replace(/[^A-Za-z0-9]+/g, "_")
24
+ .replace(/_+/g, "_")
25
+ .replace(/^_+|_+$/g, "")
26
+ .toLowerCase();
27
+ if (!s)
28
+ s = "tool";
29
+ if (/^[0-9]/.test(s))
30
+ s = `_${s}`;
31
+ return s;
32
+ }
33
+ function pascalCase(raw) {
34
+ const parts = sanitizeIdentifier(raw).split("_").filter(Boolean);
35
+ const out = parts.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
36
+ return out || "Tool";
37
+ }
38
+ function camelCase(raw) {
39
+ const p = pascalCase(raw);
40
+ return p.charAt(0).toLowerCase() + p.slice(1);
41
+ }
42
+ /** Collapse whitespace so descriptions are safe as inline comments/docstrings. */
43
+ function cleanDescription(raw) {
44
+ if (!raw)
45
+ return "";
46
+ return String(raw).replace(/\s+/g, " ").trim();
47
+ }
48
+ /** Drop duplicate params (same sanitized name) which would break codegen. */
49
+ function dedupeByName(items) {
50
+ const seen = new Set();
51
+ const out = [];
52
+ for (const item of items) {
53
+ if (seen.has(item.name))
54
+ continue;
55
+ seen.add(item.name);
56
+ out.push(item);
57
+ }
58
+ return out;
59
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "mcpfoundry",
3
+ "version": "0.1.0",
4
+ "description": "Forge production-ready MCP (Model Context Protocol) servers from databases or OpenAPI specs, with an optional ZTAI zero-trust security shield.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "scaffold",
9
+ "generator",
10
+ "openapi",
11
+ "postgres",
12
+ "ai",
13
+ "security"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "commonjs",
17
+ "bin": {
18
+ "mcpfoundry": "dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "templates",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "lint": "eslint src",
29
+ "prepare": "npm run build"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "@apidevtools/swagger-parser": "^10.1.0",
36
+ "commander": "^12.1.0",
37
+ "handlebars": "^4.7.8",
38
+ "pg": "^8.11.5"
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.9.0",
42
+ "@types/node": "^20.14.0",
43
+ "@types/pg": "^8.11.6",
44
+ "eslint": "^9.9.0",
45
+ "typescript": "^5.5.4",
46
+ "typescript-eslint": "^8.2.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/FidarOrg/mcpfoundry.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/FidarOrg/mcpfoundry/issues"
54
+ },
55
+ "homepage": "https://github.com/FidarOrg/mcpfoundry#readme"
56
+ }
@@ -0,0 +1,14 @@
1
+ # Environment for {{projectName}} (generated by mcpfoundry)
2
+ {{#if isDatabase}}
3
+ # Connection string used by the generated tool handlers.
4
+ DATABASE_URL=
5
+ {{/if}}
6
+ {{#if secure}}
7
+ # --- ZTAI Security Shield ---
8
+ # Shared HMAC secret used to verify the ZTAI auth token (HS256). Change in prod.
9
+ JWT_SECRET=dev-secret-key-change-in-production-12345
10
+ # Short-lived JWT presented over stdio. Invalid/missing => server refuses to start.
11
+ ZTAI_AUTH_TOKEN=
12
+ # Optional deception canary. When set, tool output carries a traceable marker.
13
+ ZTAI_CANARY_ID=
14
+ {{/if}}
@@ -0,0 +1,56 @@
1
+ # {{projectName}}
2
+
3
+ An MCP (Model Context Protocol) server generated by [mcpfoundry](https://www.npmjs.com/package/mcpfoundry) from a **{{sourceType}}** source.
4
+
5
+ It exposes **{{tools.length}} tool(s)** over stdio.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npm install
11
+ npm run build
12
+ npm start
13
+ ```
14
+
15
+ For development with auto-reload:
16
+
17
+ ```bash
18
+ npm run dev
19
+ ```
20
+
21
+ ## Tools
22
+
23
+ {{#each tools}}
24
+ - `{{this.name}}` — {{this.description}}
25
+ {{/each}}
26
+
27
+ Each tool validates its parameters with [Zod](https://zod.dev) before any handler
28
+ logic runs. The handlers currently contain `TODO` stubs — wire them to your real
29
+ data source.
30
+
31
+ {{#if secure}}
32
+ ## ZTAI Security Shield (enabled)
33
+
34
+ This server was generated with `--secure`, so it enforces:
35
+
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.
38
+ 2. **Parameter hardening** — Zod schemas reject malformed input.
39
+ 3. **Deception canary** — set `ZTAI_CANARY_ID` to append a traceable marker to
40
+ tool output (see `src/security.ts`).
41
+
42
+ Copy `.env.example` to `.env` and fill in the values:
43
+
44
+ ```bash
45
+ cp .env.example .env
46
+ ```
47
+ {{else}}
48
+ ## Security
49
+
50
+ This is a standard MCP server. For zero-trust auth (JWT guard) and a deception
51
+ canary, regenerate with `mcpfoundry ... --secure`, or place it behind the ZTAI
52
+ firewall.
53
+ {{/if}}
54
+
55
+ ---
56
+ Generated by mcpfoundry.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "MCP server generated by mcpfoundry.",
6
+ "type": "module",
7
+ "bin": {
8
+ "{{projectName}}": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.4.0",
17
+ "zod": "^3.23.8"{{#if secure}},
18
+ "jsonwebtoken": "^9.0.2"{{/if}}{{#if isDatabase}},
19
+ "pg": "^8.11.5"{{/if}}
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.5.4",
23
+ "tsx": "^4.16.0",
24
+ "@types/node": "^20.14.0"{{#if secure}},
25
+ "@types/jsonwebtoken": "^9.0.6"{{/if}}{{#if isDatabase}},
26
+ "@types/pg": "^8.11.6"{{/if}}
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { tools } from "./tools.js";
5
+ {{#if secure}}import { verifyStartupToken } from "./security.js";
6
+ {{/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();
11
+ {{/if}}
12
+ const server = new McpServer({ name: {{json projectName}}, version: "0.1.0" });
13
+
14
+ for (const tool of tools) {
15
+ server.tool(tool.name, tool.description, tool.schema, tool.handler);
16
+ }
17
+
18
+ const transport = new StdioServerTransport();
19
+ await server.connect(transport);
20
+ console.error(
21
+ `[{{projectName}}] MCP server running over stdio with ${tools.length} tool(s).`,
22
+ );
23
+ }
24
+
25
+ main().catch((err) => {
26
+ console.error("Fatal error starting MCP server:", err);
27
+ process.exit(1);
28
+ });
@@ -0,0 +1,66 @@
1
+ {{#if secure}}
2
+ /**
3
+ * ZTAI Security Shield (opt-in, generated by mcpfoundry --secure).
4
+ *
5
+ * Three mechanisms, mirroring the ZTAI firewall's conventions:
6
+ * 1. JWT Guard — verify a short-lived HS256 token before serving.
7
+ * 2. (Parameter hardening lives in tools.ts via Zod — always on.)
8
+ * 3. Deception Canary — append a traceable marker to tool output.
9
+ */
10
+ import crypto from "node:crypto";
11
+ import jwt from "jsonwebtoken";
12
+
13
+ export interface ZtaiTokenPayload {
14
+ agentId?: string;
15
+ orgId?: string;
16
+ iat?: number;
17
+ exp?: number;
18
+ }
19
+
20
+ /**
21
+ * stdio transport: there are no per-request headers, so the short-lived token
22
+ * is read from ZTAI_AUTH_TOKEN and verified once at startup. A missing/invalid
23
+ * token throws, which drops the connection before any tool can be reached.
24
+ */
25
+ export function verifyStartupToken(): ZtaiTokenPayload {
26
+ const secret = process.env.JWT_SECRET;
27
+ const token = process.env.ZTAI_AUTH_TOKEN;
28
+ if (!secret) {
29
+ throw new Error("ZTAI Shield: JWT_SECRET is not set. Refusing to start.");
30
+ }
31
+ if (!token) {
32
+ throw new Error("ZTAI Shield: ZTAI_AUTH_TOKEN is missing. Refusing to start.");
33
+ }
34
+ try {
35
+ return jwt.verify(token, secret, { algorithms: ["HS256"] }) as ZtaiTokenPayload;
36
+ } catch (err) {
37
+ throw new Error(
38
+ `ZTAI Shield: invalid ZTAI_AUTH_TOKEN (${(err as Error).message}). Connection dropped.`,
39
+ );
40
+ }
41
+ }
42
+
43
+ /**
44
+ * SSE/HTTP transport: verify a Bearer token from the Authorization header.
45
+ * Wire this into your HTTP layer if you swap StdioServerTransport for SSE.
46
+ */
47
+ export function verifyAuthHeader(authorization?: string): ZtaiTokenPayload {
48
+ const secret = process.env.JWT_SECRET;
49
+ if (!secret) throw new Error("ZTAI Shield: JWT_SECRET is not set.");
50
+ const match = /^Bearer\s+(.+)$/i.exec(authorization ?? "");
51
+ if (!match) throw new Error("ZTAI Shield: missing Bearer token.");
52
+ return jwt.verify(match[1], secret, { algorithms: ["HS256"] }) as ZtaiTokenPayload;
53
+ }
54
+
55
+ /**
56
+ * Deception Canary Anchor. When ZTAI_CANARY_ID is set, append a subtle,
57
+ * deterministic marker derived from it so adversarial exfiltration of tool
58
+ * output can be traced downstream. No-op when the env var is unset.
59
+ */
60
+ export function applyCanary(text: string): string {
61
+ const canaryId = process.env.ZTAI_CANARY_ID;
62
+ if (!canaryId) return text;
63
+ const seed = crypto.createHash("sha256").update(canaryId).digest("hex").slice(0, 16);
64
+ return `${text}\n<!-- ztai-canary-${seed} -->`;
65
+ }
66
+ {{/if}}
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ {{#if secure}}import { applyCanary } from "./security.js";
3
+ {{/if}}
4
+ /**
5
+ * Tools generated by mcpfoundry from a {{sourceType}} source.
6
+ * Each tool ships a Zod schema (parameter hardening, always on) and a handler
7
+ * stub — fill in the TODO with your real query / request logic.
8
+ */
9
+ export interface ForgeTool {
10
+ name: string;
11
+ description: string;
12
+ schema: Record<string, z.ZodTypeAny>;
13
+ handler: (args: any) => Promise<{ content: { type: "text"; text: string }[] }>;
14
+ }
15
+
16
+ export const tools: ForgeTool[] = [
17
+ {{#each tools}}
18
+ {
19
+ name: {{json this.name}},
20
+ description: {{json this.description}},
21
+ schema: {
22
+ {{#each this.params}}
23
+ {{this.name}}: {{{zodType this}}},
24
+ {{/each}}
25
+ },
26
+ handler: async (args) => {
27
+ // TODO: implement {{#if this.operation}}{{this.operation}} on "{{this.table}}"{{else}}{{this.method}} {{this.path}}{{/if}}.
28
+ const result = { tool: {{json this.name}}, args };
29
+ const text = JSON.stringify(result, null, 2);
30
+ return { content: [{ type: "text", text: {{#if ../secure}}applyCanary(text){{else}}text{{/if}} }] };
31
+ },
32
+ },
33
+ {{/each}}
34
+ ];
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": false
13
+ },
14
+ "include": ["src/**/*"]
15
+ }
@@ -0,0 +1,14 @@
1
+ # Environment for {{projectName}} (generated by mcpfoundry)
2
+ {{#if isDatabase}}
3
+ # Connection string used by the generated tool handlers.
4
+ DATABASE_URL=
5
+ {{/if}}
6
+ {{#if secure}}
7
+ # --- ZTAI Security Shield ---
8
+ # Shared HMAC secret used to verify the ZTAI auth token (HS256). Change in prod.
9
+ JWT_SECRET=dev-secret-key-change-in-production-12345
10
+ # Short-lived JWT presented over stdio. Invalid/missing => server refuses to start.
11
+ ZTAI_AUTH_TOKEN=
12
+ # Optional deception canary. When set, tool output carries a traceable marker.
13
+ ZTAI_CANARY_ID=
14
+ {{/if}}
@@ -0,0 +1,51 @@
1
+ # {{projectName}}
2
+
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
+
5
+ It exposes **{{tools.length}} tool(s)** over stdio.
6
+
7
+ ## Quick start
8
+
9
+ Requires **Python 3.10+**.
10
+
11
+ ```bash
12
+ python3 -m venv .venv && source .venv/bin/activate
13
+ pip install -r requirements.txt
14
+ python server.py
15
+ ```
16
+
17
+ > Prefer packaging? `pip install -e .` also works (uses `pyproject.toml`), but
18
+ > requires a recent `pip`/`setuptools`.
19
+
20
+ ## Tools
21
+
22
+ {{#each tools}}
23
+ - `{{this.name}}` — {{this.description}}
24
+ {{/each}}
25
+
26
+ Each tool validates its parameters with a Pydantic model before any handler
27
+ logic runs. The handlers currently contain `TODO` stubs — wire them to your real
28
+ data source.
29
+
30
+ {{#if secure}}
31
+ ## ZTAI Security Shield (enabled)
32
+
33
+ This server was generated with `--secure`, so it enforces:
34
+
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.
37
+ 2. **Parameter hardening** — Pydantic models reject malformed input.
38
+ 3. **Deception canary** — set `ZTAI_CANARY_ID` to append a traceable marker to
39
+ tool output (see `apply_canary` in `server.py`).
40
+
41
+ Copy `.env.example` to `.env` and fill in the values.
42
+ {{else}}
43
+ ## Security
44
+
45
+ This is a standard MCP server. For zero-trust auth (JWT guard) and a deception
46
+ canary, regenerate with `mcpfoundry ... --secure`, or place it behind the ZTAI
47
+ firewall.
48
+ {{/if}}
49
+
50
+ ---
51
+ Generated by mcpfoundry.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .env
8
+ .DS_Store
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "{{projectName}}"
3
+ version = "0.1.0"
4
+ description = "MCP server generated by mcpfoundry."
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "fastmcp>=2.0.0",
8
+ "pydantic>=2.0.0",
9
+ {{#if secure}}
10
+ "pyjwt>=2.8.0",
11
+ {{/if}}
12
+ {{#if isDatabase}}
13
+ "psycopg[binary]>=3.1.0",
14
+ {{/if}}
15
+ ]
16
+
17
+ [project.scripts]
18
+ {{projectName}} = "server:main"
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=61"]
22
+ build-backend = "setuptools.build_meta"
23
+
24
+ [tool.setuptools]
25
+ py-modules = ["server"]
@@ -0,0 +1,5 @@
1
+ fastmcp>=2.0.0
2
+ pydantic>=2.0.0
3
+ {{#if secure}}pyjwt>=2.8.0
4
+ {{/if}}{{#if isDatabase}}psycopg[binary]>=3.1.0
5
+ {{/if}}
@@ -0,0 +1,74 @@
1
+ """{{projectName}} — an MCP server generated by mcpfoundry from a {{sourceType}} source.
2
+
3
+ Each tool validates its parameters with Pydantic (parameter hardening, always
4
+ on) and contains a TODO stub — wire it to your real data source.
5
+ """
6
+ import json
7
+ import os
8
+ from typing import Annotated, Any
9
+ {{#if secure}}
10
+ import hashlib
11
+
12
+ import jwt
13
+ {{/if}}
14
+ from fastmcp import FastMCP
15
+ from pydantic import Field
16
+
17
+ mcp = FastMCP({{json projectName}})
18
+ {{#if secure}}
19
+
20
+ # --- ZTAI Security Shield (opt-in) ---
21
+
22
+ def verify_startup_token() -> dict[str, Any]:
23
+ """Verify the short-lived HS256 token before serving any tools.
24
+
25
+ stdio has no per-request headers, so the token comes from ZTAI_AUTH_TOKEN
26
+ and is verified once at startup. Missing/invalid => the server refuses to run.
27
+ """
28
+ secret = os.environ.get("JWT_SECRET")
29
+ token = os.environ.get("ZTAI_AUTH_TOKEN")
30
+ if not secret:
31
+ raise RuntimeError("ZTAI Shield: JWT_SECRET is not set. Refusing to start.")
32
+ if not token:
33
+ raise RuntimeError("ZTAI Shield: ZTAI_AUTH_TOKEN is missing. Refusing to start.")
34
+ try:
35
+ return jwt.decode(token, secret, algorithms=["HS256"])
36
+ except jwt.PyJWTError as exc:
37
+ raise RuntimeError(
38
+ f"ZTAI Shield: invalid ZTAI_AUTH_TOKEN ({exc}). Connection dropped."
39
+ ) from exc
40
+
41
+
42
+ def apply_canary(text: str) -> str:
43
+ """Append a traceable deception marker when ZTAI_CANARY_ID is set."""
44
+ canary_id = os.environ.get("ZTAI_CANARY_ID")
45
+ if not canary_id:
46
+ return text
47
+ seed = hashlib.sha256(canary_id.encode()).hexdigest()[:16]
48
+ return f"{text}\n<!-- ztai-canary-{seed} -->"
49
+ {{/if}}
50
+
51
+ {{#each tools}}
52
+ @mcp.tool()
53
+ def {{this.name}}(
54
+ {{#each this.params}}
55
+ {{{pyParam this}}},
56
+ {{/each}}
57
+ ) -> str:
58
+ """{{this.description}}"""
59
+ # TODO: implement {{#if this.operation}}{{this.operation}} on "{{this.table}}"{{else}}{{this.method}} {{this.path}}{{/if}}.
60
+ args = {{{pyArgsDict this.params}}}
61
+ result = json.dumps({"tool": {{json this.name}}, "args": args}, indent=2)
62
+ return {{#if ../secure}}apply_canary(result){{else}}result{{/if}}
63
+
64
+
65
+ {{/each}}
66
+ def main() -> None:
67
+ {{#if secure}}
68
+ verify_startup_token()
69
+ {{/if}}
70
+ mcp.run()
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()