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.
- package/README.md +228 -0
- package/bin/ont.ts +5 -0
- package/dist/bin/ont.d.ts +2 -0
- package/dist/bin/ont.js +13667 -0
- package/dist/index.js +23152 -0
- package/dist/src/browser/server.d.ts +16 -0
- package/dist/src/browser/transform.d.ts +87 -0
- package/dist/src/cli/commands/init.d.ts +12 -0
- package/dist/src/cli/commands/review.d.ts +17 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/utils/config-loader.d.ts +13 -0
- package/dist/src/config/categorical.d.ts +76 -0
- package/dist/src/config/define.d.ts +46 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/schema.d.ts +162 -0
- package/dist/src/config/types.d.ts +94 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/lockfile/differ.d.ts +11 -0
- package/dist/src/lockfile/hasher.d.ts +31 -0
- package/dist/src/lockfile/index.d.ts +53 -0
- package/dist/src/lockfile/types.d.ts +90 -0
- package/dist/src/runtime/index.d.ts +28 -0
- package/dist/src/server/api/index.d.ts +20 -0
- package/dist/src/server/api/middleware.d.ts +34 -0
- package/dist/src/server/api/router.d.ts +18 -0
- package/dist/src/server/mcp/index.d.ts +23 -0
- package/dist/src/server/mcp/tools.d.ts +35 -0
- package/dist/src/server/resolver.d.ts +30 -0
- package/dist/src/server/start.d.ts +37 -0
- package/package.json +63 -0
- package/src/browser/server.ts +2567 -0
- package/src/browser/transform.ts +473 -0
- package/src/cli/commands/init.ts +226 -0
- package/src/cli/commands/review.ts +126 -0
- package/src/cli/index.ts +19 -0
- package/src/cli/utils/config-loader.ts +78 -0
- package/src/config/categorical.ts +101 -0
- package/src/config/define.ts +78 -0
- package/src/config/index.ts +23 -0
- package/src/config/schema.ts +196 -0
- package/src/config/types.ts +121 -0
- package/src/index.ts +53 -0
- package/src/lockfile/differ.ts +242 -0
- package/src/lockfile/hasher.ts +175 -0
- package/src/lockfile/index.ts +159 -0
- package/src/lockfile/types.ts +95 -0
- package/src/runtime/index.ts +114 -0
- package/src/server/api/index.ts +92 -0
- package/src/server/api/middleware.ts +118 -0
- package/src/server/api/router.ts +102 -0
- package/src/server/mcp/index.ts +182 -0
- package/src/server/mcp/tools.ts +199 -0
- package/src/server/resolver.ts +109 -0
- 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
|
+
}
|