@@ -80,6 +80,14 @@ export interface GeneratorOptions {
8080 validator ?: ValidatorTarget | undefined ;
8181 /** Treat extensible bindings as closed enums in validators (default: open). */
8282 strictExtensible ?: boolean | undefined ;
83+ /**
84+ * Phase 8.8 — when set, emit an MCP server scaffold under this directory
85+ * containing `mcp.config.json` (resource list, IG reference) and a
86+ * `server.ts` shim that calls `@fhir-dsl/mcp`'s `createServer`. The
87+ * scaffold reads the upstream base URL from the FHIR_BASE_URL env var
88+ * at runtime so the generated artefacts stay portable.
89+ */
90+ mcpOutDir ?: string | undefined ;
8391}
8492
8593export async function generate ( options : GeneratorOptions ) : Promise < void > {
@@ -427,6 +435,16 @@ export async function generate(options: GeneratorOptions): Promise<void> {
427435 }
428436 }
429437
438+ // --- Phase 8.8: MCP server scaffold ---
439+ if ( options . mcpOutDir ) {
440+ await emitMcpScaffold ( {
441+ outDir : options . mcpOutDir ,
442+ version,
443+ igRefs : igPackages ,
444+ resourceTypes : resourceModels . map ( ( r ) => r . name ) ,
445+ } ) ;
446+ }
447+
430448 // --- Spec index ---
431449 if ( options . includeSpec ) {
432450 const specDir = join ( versionDir , "spec" ) ;
@@ -483,3 +501,96 @@ async function fileExists(path: string): Promise<boolean> {
483501 return false ;
484502 }
485503}
504+
505+ interface McpScaffoldOptions {
506+ outDir : string ;
507+ version : string ;
508+ igRefs : readonly string [ ] ;
509+ resourceTypes : readonly string [ ] ;
510+ }
511+
512+ async function emitMcpScaffold ( opts : McpScaffoldOptions ) : Promise < void > {
513+ await mkdir ( opts . outDir , { recursive : true } ) ;
514+
515+ const config = {
516+ version : opts . version ,
517+ igRefs : [ ...opts . igRefs ] ,
518+ resourceTypes : [ ...opts . resourceTypes ] . sort ( ) ,
519+ writes : [ ] as string [ ] ,
520+ confirmWrites : true ,
521+ dryRun : false ,
522+ defaultSearchCount : 20 ,
523+ maxResponseBytes : 65536 ,
524+ } ;
525+ await writeFile ( join ( opts . outDir , "mcp.config.json" ) , `${ JSON . stringify ( config , null , 2 ) } \n` , "utf-8" ) ;
526+
527+ const serverContents = `import { readFileSync } from "node:fs";
528+ import { fileURLToPath } from "node:url";
529+ import { dirname, resolve } from "node:path";
530+ import { createServer, JsonLogAuditSink, stdioTransport } from "@fhir-dsl/mcp";
531+
532+ const __dirname = dirname(fileURLToPath(import.meta.url));
533+ const config = JSON.parse(readFileSync(resolve(__dirname, "mcp.config.json"), "utf-8"));
534+
535+ const baseUrl = process.env.FHIR_BASE_URL;
536+ if (!baseUrl) {
537+ console.error("Set FHIR_BASE_URL to the upstream FHIR endpoint before launching the MCP server.");
538+ process.exit(1);
539+ }
540+
541+ const token = process.env.FHIR_TOKEN;
542+
543+ const server = createServer({
544+ name: "${ opts . igRefs [ 0 ] ?. replace ( / [ @ / ] / g, "-" ) ?? `fhir-${ opts . version } ` } -mcp",
545+ version: "0.1.0",
546+ baseUrl,
547+ resourceTypes: config.resourceTypes,
548+ audit: new JsonLogAuditSink(),
549+ ...(config.writes && config.writes.length > 0 ? { writes: config.writes } : {}),
550+ ...(config.confirmWrites ? { confirmWrites: true } : {}),
551+ ...(config.dryRun ? { dryRun: true } : {}),
552+ ...(config.defaultSearchCount !== undefined ? { defaultSearchCount: config.defaultSearchCount } : {}),
553+ ...(config.maxResponseBytes !== undefined ? { maxResponseBytes: config.maxResponseBytes } : {}),
554+ ...(token ? { auth: { kind: "bearer" as const, token } } : {}),
555+ });
556+
557+ console.error(\`[fhir-dsl-mcp] starting stdio server for \${baseUrl} (\${config.resourceTypes.length} resource type(s))\`);
558+ await server.listen(stdioTransport());
559+ ` ;
560+ await writeFile ( join ( opts . outDir , "server.ts" ) , serverContents , "utf-8" ) ;
561+
562+ const readme = `# Generated FHIR MCP server
563+
564+ This directory was emitted by \`fhir-gen generate --mcp <out>\` and is
565+ ready to launch as an MCP stdio server.
566+
567+ ## Run
568+
569+ \`\`\`sh
570+ FHIR_BASE_URL=https://your-fhir-server/baseR${ opts . version . replace ( / ^ r / i, "" ) . toUpperCase ( ) } \\
571+ FHIR_TOKEN=optional-bearer-token \\
572+ node --experimental-strip-types server.ts
573+ \`\`\`
574+
575+ (or compile \`server.ts\` to JS first if your runtime doesn't support
576+ type stripping).
577+
578+ ## Configuration
579+
580+ \`mcp.config.json\` controls the resource surface, write gating, and
581+ token-economy defaults. Edit it to:
582+
583+ - Add a \`writes\` whitelist (e.g. \`["create", "update"]\`) to expose
584+ write tools.
585+ - Tighten or relax \`maxResponseBytes\` and \`defaultSearchCount\`.
586+ - Disable \`confirmWrites\` if your agent doesn't need explicit
587+ confirmation on every write.
588+
589+ ## IGs bound
590+
591+ ${ opts . igRefs . length > 0 ? opts . igRefs . map ( ( r ) => `- \`${ r } \`` ) . join ( "\n" ) : "_(no IGs — base FHIR ${opts.version})_" }
592+ ` ;
593+ await writeFile ( join ( opts . outDir , "README.md" ) , readme , "utf-8" ) ;
594+
595+ console . info ( `Generated MCP scaffold in ${ opts . outDir } ` ) ;
596+ }
0 commit comments