ont-run 0.0.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.
Files changed (54) hide show
  1. package/README.md +228 -0
  2. package/bin/ont.ts +5 -0
  3. package/dist/bin/ont.d.ts +2 -0
  4. package/dist/bin/ont.js +13667 -0
  5. package/dist/index.js +23152 -0
  6. package/dist/src/browser/server.d.ts +16 -0
  7. package/dist/src/browser/transform.d.ts +87 -0
  8. package/dist/src/cli/commands/init.d.ts +12 -0
  9. package/dist/src/cli/commands/review.d.ts +17 -0
  10. package/dist/src/cli/index.d.ts +1 -0
  11. package/dist/src/cli/utils/config-loader.d.ts +13 -0
  12. package/dist/src/config/categorical.d.ts +76 -0
  13. package/dist/src/config/define.d.ts +46 -0
  14. package/dist/src/config/index.d.ts +4 -0
  15. package/dist/src/config/schema.d.ts +162 -0
  16. package/dist/src/config/types.d.ts +94 -0
  17. package/dist/src/index.d.ts +37 -0
  18. package/dist/src/lockfile/differ.d.ts +11 -0
  19. package/dist/src/lockfile/hasher.d.ts +31 -0
  20. package/dist/src/lockfile/index.d.ts +53 -0
  21. package/dist/src/lockfile/types.d.ts +90 -0
  22. package/dist/src/runtime/index.d.ts +28 -0
  23. package/dist/src/server/api/index.d.ts +20 -0
  24. package/dist/src/server/api/middleware.d.ts +34 -0
  25. package/dist/src/server/api/router.d.ts +18 -0
  26. package/dist/src/server/mcp/index.d.ts +23 -0
  27. package/dist/src/server/mcp/tools.d.ts +35 -0
  28. package/dist/src/server/resolver.d.ts +30 -0
  29. package/dist/src/server/start.d.ts +37 -0
  30. package/package.json +63 -0
  31. package/src/browser/server.ts +2567 -0
  32. package/src/browser/transform.ts +473 -0
  33. package/src/cli/commands/init.ts +226 -0
  34. package/src/cli/commands/review.ts +126 -0
  35. package/src/cli/index.ts +19 -0
  36. package/src/cli/utils/config-loader.ts +78 -0
  37. package/src/config/categorical.ts +101 -0
  38. package/src/config/define.ts +78 -0
  39. package/src/config/index.ts +23 -0
  40. package/src/config/schema.ts +196 -0
  41. package/src/config/types.ts +121 -0
  42. package/src/index.ts +53 -0
  43. package/src/lockfile/differ.ts +242 -0
  44. package/src/lockfile/hasher.ts +175 -0
  45. package/src/lockfile/index.ts +159 -0
  46. package/src/lockfile/types.ts +95 -0
  47. package/src/runtime/index.ts +114 -0
  48. package/src/server/api/index.ts +92 -0
  49. package/src/server/api/middleware.ts +118 -0
  50. package/src/server/api/router.ts +102 -0
  51. package/src/server/mcp/index.ts +182 -0
  52. package/src/server/mcp/tools.ts +199 -0
  53. package/src/server/resolver.ts +109 -0
  54. package/src/server/start.ts +151 -0
@@ -0,0 +1,182 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from "@modelcontextprotocol/sdk/types.js";
7
+ import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
8
+ import { Hono } from "hono";
9
+ import { cors } from "hono/cors";
10
+ import type { OntologyConfig, ResolverContext, EnvironmentConfig } from "../../config/types.js";
11
+ import { generateMcpTools, filterToolsByAccess, createToolExecutor } from "./tools.js";
12
+ import { createLogger } from "../resolver.js";
13
+ import { serve } from "../../runtime/index.js";
14
+
15
+ /**
16
+ * Extract access groups from AuthInfo
17
+ */
18
+ function getAccessGroups(authInfo?: AuthInfo): string[] {
19
+ if (!authInfo?.extra?.accessGroups) return [];
20
+ return authInfo.extra.accessGroups as string[];
21
+ }
22
+
23
+ export interface McpServerOptions {
24
+ /** The ontology configuration */
25
+ config: OntologyConfig;
26
+ /** Directory containing the ontology.config.ts */
27
+ configDir: string;
28
+ /** Environment to use */
29
+ env: string;
30
+ /** Port for the MCP HTTP server */
31
+ port?: number;
32
+ }
33
+
34
+ /**
35
+ * Create the MCP server instance with per-request authentication
36
+ */
37
+ export function createMcpServer(options: McpServerOptions): Server {
38
+ const { config, configDir, env } = options;
39
+
40
+ // Get environment config
41
+ const envConfig = config.environments[env];
42
+ if (!envConfig) {
43
+ throw new Error(
44
+ `Unknown environment "${env}". Available: ${Object.keys(config.environments).join(", ")}`
45
+ );
46
+ }
47
+
48
+ const logger = createLogger(envConfig.debug);
49
+
50
+ // Generate all tools (filtering happens per-request)
51
+ const allTools = generateMcpTools(config);
52
+
53
+ // Create tool executor factory that accepts per-request access groups
54
+ const executeToolWithAccess = createToolExecutor(config, configDir, env, envConfig, logger);
55
+
56
+ // Create MCP server
57
+ const server = new Server(
58
+ {
59
+ name: config.name,
60
+ version: "1.0.0",
61
+ },
62
+ {
63
+ capabilities: {
64
+ tools: {},
65
+ },
66
+ }
67
+ );
68
+
69
+ // Handle list tools request - filter by per-request access groups
70
+ server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
71
+ const accessGroups = getAccessGroups(extra.authInfo);
72
+ const accessibleTools = filterToolsByAccess(allTools, accessGroups);
73
+
74
+ return {
75
+ tools: accessibleTools.map((tool) => ({
76
+ name: tool.name,
77
+ description: tool.description,
78
+ inputSchema: tool.inputSchema,
79
+ })),
80
+ };
81
+ });
82
+
83
+ // Handle call tool request - validate access per-request
84
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
85
+ const { name, arguments: args } = request.params;
86
+ const accessGroups = getAccessGroups(extra.authInfo);
87
+
88
+ try {
89
+ const result = await executeToolWithAccess(name, args || {}, accessGroups);
90
+
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text" as const,
95
+ text: JSON.stringify(result, null, 2),
96
+ },
97
+ ],
98
+ };
99
+ } catch (error) {
100
+ const message = error instanceof Error ? error.message : "Unknown error";
101
+
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text" as const,
106
+ text: `Error: ${message}`,
107
+ },
108
+ ],
109
+ isError: true,
110
+ };
111
+ }
112
+ });
113
+
114
+ return server;
115
+ }
116
+
117
+ /**
118
+ * Start the MCP server as an HTTP server with Streamable HTTP transport
119
+ */
120
+ export async function startMcpServer(options: McpServerOptions): Promise<{ port: number }> {
121
+ const { config, port = 3001 } = options;
122
+ const server = createMcpServer(options);
123
+
124
+ // Create a stateless transport
125
+ const transport = new WebStandardStreamableHTTPServerTransport();
126
+
127
+ // Connect server to transport
128
+ await server.connect(transport);
129
+
130
+ const app = new Hono();
131
+
132
+ // Enable CORS for MCP clients
133
+ app.use("*", cors({
134
+ origin: "*",
135
+ allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
136
+ allowHeaders: ["Content-Type", "mcp-session-id", "Last-Event-ID", "mcp-protocol-version", "Authorization"],
137
+ exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
138
+ }));
139
+
140
+ // Health check
141
+ app.get("/health", (c) => {
142
+ return c.json({ status: "ok", type: "mcp" });
143
+ });
144
+
145
+ // MCP endpoint - handles all MCP communication with per-request auth
146
+ app.all("/mcp", async (c) => {
147
+ try {
148
+ // Authenticate the request using the config's auth function
149
+ const accessGroups = await config.auth(c.req.raw);
150
+
151
+ // Create AuthInfo object with access groups in extra field
152
+ const authInfo: AuthInfo = {
153
+ token: c.req.header("Authorization") || "",
154
+ clientId: "ontology-client",
155
+ scopes: [],
156
+ extra: {
157
+ accessGroups,
158
+ },
159
+ };
160
+
161
+ // Pass auth info to transport - this will be available in request handlers via extra.authInfo
162
+ return transport.handleRequest(c.req.raw, { authInfo });
163
+ } catch (error) {
164
+ // Return 401 Unauthorized if auth fails
165
+ return c.json({
166
+ jsonrpc: "2.0",
167
+ error: {
168
+ code: -32000,
169
+ message: "Authentication failed",
170
+ data: error instanceof Error ? error.message : "Unknown error"
171
+ },
172
+ id: null
173
+ }, 401);
174
+ }
175
+ });
176
+
177
+ const httpServer = await serve(app, port);
178
+
179
+ return { port: httpServer.port };
180
+ }
181
+
182
+ export { generateMcpTools, filterToolsByAccess } from "./tools.js";
@@ -0,0 +1,199 @@
1
+ import { z } from "zod";
2
+ import { zodToJsonSchema } from "zod-to-json-schema";
3
+ import type {
4
+ OntologyConfig,
5
+ FunctionDefinition,
6
+ ResolverContext,
7
+ EnvironmentConfig,
8
+ } from "../../config/types.js";
9
+ import { getFieldFromMetadata } from "../../config/categorical.js";
10
+ import { loadResolver, type Logger } from "../resolver.js";
11
+
12
+ /**
13
+ * Field reference info for MCP tools
14
+ */
15
+ export interface McpFieldReference {
16
+ /** Path to the field in the schema */
17
+ path: string;
18
+ /** Name of the function that provides options */
19
+ functionName: string;
20
+ }
21
+
22
+ /**
23
+ * MCP Tool definition
24
+ */
25
+ export interface McpTool {
26
+ name: string;
27
+ description: string;
28
+ inputSchema: Record<string, unknown>;
29
+ outputSchema?: Record<string, unknown>;
30
+ access: string[];
31
+ entities: string[];
32
+ fieldReferences?: McpFieldReference[];
33
+ }
34
+
35
+ /**
36
+ * Recursively extract field references from a Zod schema
37
+ */
38
+ function extractFieldReferencesForMcp(
39
+ schema: z.ZodType<unknown>,
40
+ path: string = ""
41
+ ): McpFieldReference[] {
42
+ const results: McpFieldReference[] = [];
43
+
44
+ // Check if this schema has fieldFrom metadata
45
+ const metadata = getFieldFromMetadata(schema);
46
+ if (metadata) {
47
+ results.push({
48
+ path: path || "(root)",
49
+ functionName: metadata.functionName,
50
+ });
51
+ }
52
+
53
+ // Handle ZodObject - recurse into properties
54
+ if (schema instanceof z.ZodObject) {
55
+ const shape = schema.shape;
56
+ for (const [key, value] of Object.entries(shape)) {
57
+ const fieldPath = path ? `${path}.${key}` : key;
58
+ results.push(
59
+ ...extractFieldReferencesForMcp(value as z.ZodType<unknown>, fieldPath)
60
+ );
61
+ }
62
+ }
63
+
64
+ // Handle ZodOptional - unwrap
65
+ if (schema instanceof z.ZodOptional) {
66
+ results.push(...extractFieldReferencesForMcp(schema.unwrap(), path));
67
+ }
68
+
69
+ // Handle ZodNullable - unwrap
70
+ if (schema instanceof z.ZodNullable) {
71
+ results.push(...extractFieldReferencesForMcp(schema.unwrap(), path));
72
+ }
73
+
74
+ // Handle ZodArray - recurse into element
75
+ if (schema instanceof z.ZodArray) {
76
+ results.push(...extractFieldReferencesForMcp(schema.element, `${path}[]`));
77
+ }
78
+
79
+ // Handle ZodDefault - unwrap
80
+ if (schema instanceof z.ZodDefault) {
81
+ results.push(
82
+ ...extractFieldReferencesForMcp(schema._def.innerType, path)
83
+ );
84
+ }
85
+
86
+ return results;
87
+ }
88
+
89
+ /**
90
+ * Generate MCP tool definitions from ontology config
91
+ */
92
+ export function generateMcpTools(config: OntologyConfig): McpTool[] {
93
+ const tools: McpTool[] = [];
94
+
95
+ for (const [name, fn] of Object.entries(config.functions)) {
96
+ // Convert Zod schema to JSON Schema for MCP
97
+ let inputSchema: Record<string, unknown>;
98
+ try {
99
+ inputSchema = zodToJsonSchema(fn.inputs, {
100
+ $refStrategy: "none",
101
+ }) as Record<string, unknown>;
102
+ // Remove $schema key if present
103
+ delete inputSchema.$schema;
104
+ } catch {
105
+ inputSchema = { type: "object", properties: {} };
106
+ }
107
+
108
+ // Convert output schema if present
109
+ let outputSchema: Record<string, unknown> | undefined;
110
+ if (fn.outputs) {
111
+ try {
112
+ outputSchema = zodToJsonSchema(fn.outputs, {
113
+ $refStrategy: "none",
114
+ }) as Record<string, unknown>;
115
+ delete outputSchema.$schema;
116
+ } catch {
117
+ outputSchema = undefined;
118
+ }
119
+ }
120
+
121
+ // Extract field references
122
+ const fieldReferences = extractFieldReferencesForMcp(fn.inputs);
123
+
124
+ tools.push({
125
+ name,
126
+ description: fn.description,
127
+ inputSchema,
128
+ outputSchema,
129
+ access: fn.access,
130
+ entities: fn.entities,
131
+ fieldReferences:
132
+ fieldReferences.length > 0 ? fieldReferences : undefined,
133
+ });
134
+ }
135
+
136
+ return tools;
137
+ }
138
+
139
+ /**
140
+ * Filter tools by access groups
141
+ */
142
+ export function filterToolsByAccess(
143
+ tools: McpTool[],
144
+ accessGroups: string[]
145
+ ): McpTool[] {
146
+ return tools.filter((tool) =>
147
+ tool.access.some((group) => accessGroups.includes(group))
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Create a tool executor function that accepts per-request access groups
153
+ */
154
+ export function createToolExecutor(
155
+ config: OntologyConfig,
156
+ configDir: string,
157
+ env: string,
158
+ envConfig: EnvironmentConfig,
159
+ logger: Logger
160
+ ) {
161
+ return async (toolName: string, args: unknown, accessGroups: string[]): Promise<unknown> => {
162
+ const fn = config.functions[toolName];
163
+
164
+ if (!fn) {
165
+ throw new Error(`Unknown tool: ${toolName}`);
166
+ }
167
+
168
+ // Check access using per-request access groups
169
+ const hasAccess = fn.access.some((group) =>
170
+ accessGroups.includes(group)
171
+ );
172
+
173
+ if (!hasAccess) {
174
+ throw new Error(
175
+ `Access denied to tool "${toolName}". Requires: ${fn.access.join(", ")}`
176
+ );
177
+ }
178
+
179
+ // Validate input
180
+ const parsed = fn.inputs.safeParse(args);
181
+ if (!parsed.success) {
182
+ throw new Error(
183
+ `Invalid input for tool "${toolName}": ${parsed.error.message}`
184
+ );
185
+ }
186
+
187
+ // Create resolver context with per-request access groups
188
+ const resolverContext: ResolverContext = {
189
+ env,
190
+ envConfig,
191
+ logger,
192
+ accessGroups,
193
+ };
194
+
195
+ // Load and execute resolver
196
+ const resolver = await loadResolver(fn.resolver, configDir);
197
+ return resolver(resolverContext, parsed.data);
198
+ };
199
+ }
@@ -0,0 +1,109 @@
1
+ import { join, dirname, isAbsolute } from "path";
2
+ import { existsSync } from "fs";
3
+ import type { ResolverFunction, ResolverContext, OntologyConfig } from "../config/types.js";
4
+
5
+ /**
6
+ * Cache of loaded resolvers to avoid re-importing
7
+ */
8
+ const resolverCache = new Map<string, ResolverFunction>();
9
+
10
+ /**
11
+ * Load a resolver from a file path.
12
+ * The path is relative to the config file location.
13
+ *
14
+ * @param resolverPath - Path to the resolver file (relative to configDir)
15
+ * @param configDir - Directory containing the ontology.config.ts
16
+ */
17
+ export async function loadResolver(
18
+ resolverPath: string,
19
+ configDir: string
20
+ ): Promise<ResolverFunction> {
21
+ // Resolve the full path
22
+ const fullPath = isAbsolute(resolverPath)
23
+ ? resolverPath
24
+ : join(configDir, resolverPath);
25
+
26
+ // Check cache
27
+ if (resolverCache.has(fullPath)) {
28
+ return resolverCache.get(fullPath)!;
29
+ }
30
+
31
+ try {
32
+ // Dynamic import the resolver
33
+ const module = await import(fullPath);
34
+
35
+ // Expect default export to be the resolver function
36
+ const resolver = module.default;
37
+
38
+ if (typeof resolver !== "function") {
39
+ throw new Error(
40
+ `Resolver at ${resolverPath} must export a default function`
41
+ );
42
+ }
43
+
44
+ // Cache and return
45
+ resolverCache.set(fullPath, resolver);
46
+ return resolver;
47
+ } catch (error) {
48
+ if ((error as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") {
49
+ throw new Error(`Resolver not found: ${resolverPath}`);
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Clear the resolver cache (useful for hot reloading)
57
+ */
58
+ export function clearResolverCache(): void {
59
+ resolverCache.clear();
60
+ }
61
+
62
+ /**
63
+ * Check which resolvers are missing and return their paths
64
+ */
65
+ export function findMissingResolvers(
66
+ config: OntologyConfig,
67
+ configDir: string
68
+ ): string[] {
69
+ const missing: string[] = [];
70
+
71
+ for (const [name, fn] of Object.entries(config.functions)) {
72
+ const fullPath = isAbsolute(fn.resolver)
73
+ ? fn.resolver
74
+ : join(configDir, fn.resolver);
75
+
76
+ if (!existsSync(fullPath)) {
77
+ missing.push(fn.resolver);
78
+ }
79
+ }
80
+
81
+ return missing;
82
+ }
83
+
84
+ /**
85
+ * Logger type returned by createLogger
86
+ */
87
+ export type Logger = ReturnType<typeof createLogger>;
88
+
89
+ /**
90
+ * Create a logger for a resolver context
91
+ */
92
+ export function createLogger(debug: boolean = false) {
93
+ return {
94
+ info: (message: string, ...args: unknown[]) => {
95
+ console.log(`[INFO] ${message}`, ...args);
96
+ },
97
+ warn: (message: string, ...args: unknown[]) => {
98
+ console.warn(`[WARN] ${message}`, ...args);
99
+ },
100
+ error: (message: string, ...args: unknown[]) => {
101
+ console.error(`[ERROR] ${message}`, ...args);
102
+ },
103
+ debug: (message: string, ...args: unknown[]) => {
104
+ if (debug) {
105
+ console.log(`[DEBUG] ${message}`, ...args);
106
+ }
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,151 @@
1
+ import consola from "consola";
2
+ import { findConfigFile, loadConfig } from "../cli/utils/config-loader.js";
3
+ import {
4
+ computeOntologyHash,
5
+ readLockfile,
6
+ diffOntology,
7
+ formatDiffForConsole,
8
+ lockfileExists,
9
+ } from "../lockfile/index.js";
10
+ import { createApiApp } from "./api/index.js";
11
+ import { startMcpServer } from "./mcp/index.js";
12
+ import { serve, type ServerHandle } from "../runtime/index.js";
13
+
14
+ export interface StartOntOptions {
15
+ /** Port for API server (default: 3000) */
16
+ port?: number;
17
+ /** Port for MCP server (default: 3001) */
18
+ mcpPort?: number;
19
+ /** Environment to use (default: 'dev') */
20
+ env?: string;
21
+ /** Mode: 'development' warns on lockfile issues, 'production' fails. Auto-detected from NODE_ENV if not set. */
22
+ mode?: "development" | "production";
23
+ /** Set to true to only start the API server */
24
+ apiOnly?: boolean;
25
+ /** Set to true to only start the MCP server */
26
+ mcpOnly?: boolean;
27
+ }
28
+
29
+ export interface StartOntResult {
30
+ api?: ServerHandle;
31
+ mcp?: { port: number };
32
+ }
33
+
34
+ /**
35
+ * Detect mode from NODE_ENV if not explicitly set
36
+ */
37
+ function detectMode(explicit?: "development" | "production"): "development" | "production" {
38
+ if (explicit) return explicit;
39
+ return process.env.NODE_ENV === "production" ? "production" : "development";
40
+ }
41
+
42
+ /**
43
+ * Start the ont API and MCP servers.
44
+ *
45
+ * Automatically discovers ontology.config.ts and handles lockfile validation.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { startOnt } from 'ont-run';
50
+ *
51
+ * await startOnt({
52
+ * port: 3000,
53
+ * mcpPort: 3001,
54
+ * });
55
+ * ```
56
+ */
57
+ export async function startOnt(options: StartOntOptions = {}): Promise<StartOntResult> {
58
+ const {
59
+ port = 3000,
60
+ mcpPort = 3001,
61
+ env = "dev",
62
+ apiOnly = false,
63
+ mcpOnly = false,
64
+ } = options;
65
+
66
+ const mode = detectMode(options.mode);
67
+ const isDev = mode === "development";
68
+
69
+ // Load config
70
+ consola.info("Loading ontology config...");
71
+ const { config, configDir } = await loadConfig();
72
+
73
+ // Check lockfile
74
+ consola.info("Checking lockfile...");
75
+ const { ontology, hash } = computeOntologyHash(config);
76
+
77
+ if (!lockfileExists(configDir)) {
78
+ const message = `No ont.lock file found.\nRun \`bun run review\` to approve the initial ontology.`;
79
+
80
+ if (isDev) {
81
+ consola.warn("No ont.lock file found.");
82
+ consola.warn("Run `npx ont-run review` to approve the initial ontology.\n");
83
+ } else {
84
+ consola.error(message);
85
+ throw new Error("Missing lockfile in production mode");
86
+ }
87
+ } else {
88
+ const lockfile = await readLockfile(configDir);
89
+ const oldOntology = lockfile?.ontology || null;
90
+ const diff = diffOntology(oldOntology, ontology);
91
+
92
+ if (diff.hasChanges) {
93
+ const message = `Ontology has changed since last review.\nRun \`bun run review\` to approve the changes.`;
94
+
95
+ if (isDev) {
96
+ consola.warn("Lockfile mismatch detected:");
97
+ console.log("\n" + formatDiffForConsole(diff) + "\n");
98
+ consola.warn("Run `npx ont-run review` to approve these changes.\n");
99
+ } else {
100
+ consola.error(message);
101
+ console.log("\n" + formatDiffForConsole(diff) + "\n");
102
+ throw new Error("Lockfile mismatch in production mode");
103
+ }
104
+ } else {
105
+ consola.success("Lockfile verified");
106
+ }
107
+ }
108
+
109
+ const result: StartOntResult = {};
110
+
111
+ // Start API server
112
+ if (!mcpOnly) {
113
+ const api = createApiApp({
114
+ config,
115
+ configDir,
116
+ env,
117
+ });
118
+
119
+ const server = await serve(api, port);
120
+ result.api = server;
121
+
122
+ consola.success(`API server running at http://localhost:${server.port}`);
123
+ consola.info(`Environment: ${env}`);
124
+ consola.info(`Mode: ${mode}`);
125
+ consola.info(`Functions: ${Object.keys(config.functions).length}`);
126
+ console.log("");
127
+ consola.info("Available endpoints:");
128
+ for (const name of Object.keys(config.functions)) {
129
+ console.log(` POST /api/${name}`);
130
+ }
131
+ console.log("");
132
+ }
133
+
134
+ // Start MCP server
135
+ if (!apiOnly) {
136
+ const mcpServer = await startMcpServer({
137
+ config,
138
+ configDir,
139
+ env,
140
+ port: mcpPort,
141
+ });
142
+ result.mcp = mcpServer;
143
+
144
+ consola.success(`MCP server running at http://localhost:${mcpServer.port}/mcp`);
145
+ consola.info(`Auth: per-request (using config.auth)`);
146
+ }
147
+
148
+ consola.info("Press Ctrl+C to stop");
149
+
150
+ return result;
151
+ }