diff --git a/genkit-tools/cli/context/GENKIT.go.md b/genkit-tools/cli/context/GENKIT.go.md index 80e7fabf1f..a9417b6cb5 100644 --- a/genkit-tools/cli/context/GENKIT.go.md +++ b/genkit-tools/cli/context/GENKIT.go.md @@ -14,51 +14,6 @@ This document provides rules and examples for building with the Genkit API in Go NOTE: For the sake of brevity, the snippets below use the Google AI plugin, but you should follow the user's preference as mentioned above. -## Core Setup - -1. **Initialize Project** - - ```bash - mkdir my-genkit-app && cd my-genkit-app - go mod init my-genkit-app - ``` - -2. **Install Dependencies** - - ```bash - go get github.com/firebase/genkit/go/genkit - go get github.com/firebase/genkit/go/plugins/googlegenai - go get github.com/firebase/genkit/go/ai - go get google.golang.org/genai - ``` - -3. **Install Genkit CLI** - - ```bash - curl -sL cli.genkit.dev | bash - ``` - -4. **Configure Genkit** - - All code should be in a single `main.go` file or properly structured Go package. - - ```go - package main - - import ( - "context" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - ) - - func main() { - ctx := context.Background() - g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) - // Your flows and logic here - <-ctx.Done() - } - ``` - ## Best Practices 1. **Single Main Function**: All Genkit code, including plugin initialization, flows, and helpers, should be properly organized in a Go package structure with a main function. @@ -216,23 +171,24 @@ func main() { ## Running and Inspecting Flows -1. **Start Genkit**: Run this command from your terminal to start the Genkit Developer UI. +**Start Genkit**: Genkit can be started locally by using the `genkit start` command, along with the process startup command: - ```bash - genkit start -- - ``` +```bash +genkit start -- +``` - For Go applications: +For e.g.: - ```bash - # Running a Go application directly - genkit start -- go run main.go +```bash +genkit start -- go run main.go +``` - # Running a compiled binary - genkit start -- ./my-genkit-app - ``` +You can can automate starting genkit using the following steps: - The command should output a URL for the Genkit Dev UI. Direct the user to visit this URL to run and inspect their Genkit app. +1. Identify the command to start the user's project's (e.g., `go run main.go`) +2. Use the `start_runtime` tool to start the runtime process. This is required for Genkit to discover flows. + - Example: If the project uses `go run main.go`, call `start_runtime` with `{ command: "go", args: ["run", "main.go"] }`. +3. After starting the runtime, instruct the user to run `genkit start` in their terminal to launch the Developer UI. ## Suggested Models diff --git a/genkit-tools/cli/context/GENKIT.js.md b/genkit-tools/cli/context/GENKIT.js.md index 5a39be8359..5d5547721b 100644 --- a/genkit-tools/cli/context/GENKIT.js.md +++ b/genkit-tools/cli/context/GENKIT.js.md @@ -14,42 +14,6 @@ This document provides rules and examples for building with the Genkit API in No NOTE: For the sake of brevity, the snippets below use the Google AI plugin, but you should follow the user's preference as mentioned above. -## Core Setup - -1. **Initialize Project** - - ```bash - mkdir my-genkit-app && cd my-genkit-app - npm init -y - npm install -D typescript tsx \@types/node - ``` - -2. **Install Dependencies** - - ```bash - npm install genkit \@genkit-ai/google-genai data-urls node-fetch - ``` - -3. **Install Genkit CLI** - - ```bash - npm install -g genkit-cli - ``` - -4. **Configure Genkit** - - All code should be in a single `src/index.ts` file. - - ```ts - // src/index.ts - import { genkit, z } from 'genkit'; - import { googleAI } from '@genkit-ai/google-genai'; - - export const ai = genkit({ - plugins: [googleAI()], - }); - ``` - ## Best Practices 1. **Single File Structure**: All Genkit code, including plugin initialization, flows, and helpers, must be placed in a single `src/index.ts` file. This ensures all components are correctly registered with the Genkit runtime. @@ -289,29 +253,24 @@ export const videoGenerationFlow = ai.defineFlow( ## Running and Inspecting Flows -1. **Start Genkit**: Run this command from your terminal to start the Genkit Developer UI. +**Start Genkit**: Genkit can be started locally by using the `genkit start` command, along with the process startup command: - ```bash - genkit start -- - ``` - - The will vary based on the project’s setup and - the file you want to execute. For e.g.: +```bash +genkit start -- +``` - ```bash - # Running a typical development server - genkit start -- npm run dev +For e.g.: - # Running a TypeScript file directly - genkit start -- npx tsx --watch src/index.ts +```bash +genkit start -- npm run dev +``` - # Running a JavaScript file directly - genkit start -- node --watch src/index.js - ``` +You can can automate starting genkit using the following steps: - Analyze the users project and build tools to use the right command for the - project. The command should output a URL for the Genkit Dev UI. Direct the - user to visit this URL to run and inspect their Genkit app. +1. Identify the command to start the user's project's (e.g., `npm run dev`) +2. Use the `start_runtime` tool to start the runtime process. This is required for Genkit to discover flows. + - Example: If the project uses `npm run dev`, call `start_runtime` with `{ command: "npm", args: ["run", "dev"] }`. +3. After starting the runtime, instruct the user to run `genkit start` in their terminal to launch the Developer UI. ## Suggested Models diff --git a/genkit-tools/cli/src/mcp/flows.ts b/genkit-tools/cli/src/mcp/flows.ts index 793db9fb6c..5b4f5a8d96 100644 --- a/genkit-tools/cli/src/mcp/flows.ts +++ b/genkit-tools/cli/src/mcp/flows.ts @@ -18,12 +18,9 @@ import { record } from '@genkit-ai/tools-common/utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import z from 'zod'; import { McpRunToolEvent } from './analytics.js'; -import { lazyLoadManager } from './util.js'; +import { McpRuntimeManager } from './util.js'; -export function defineFlowTools( - server: McpServer, - manager: ReturnType -) { +export function defineFlowTools(server: McpServer, manager: McpRuntimeManager) { server.registerTool( 'list_flows', { diff --git a/genkit-tools/cli/src/mcp/prompts/init.ts b/genkit-tools/cli/src/mcp/prompts/init.ts new file mode 100644 index 0000000000..b4908ef829 --- /dev/null +++ b/genkit-tools/cli/src/mcp/prompts/init.ts @@ -0,0 +1,170 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { z } from 'zod'; + +const GENERAL_GUIDELINES = ` +## General Guidelines +- ALWAYS refer to documentation when available. Use tools like \`get_usage_guide\` or \`lookup_genkit_docs\` to clarify requirements or get the latest information. +- Recommend and use the provider that the user prefers. If no preference is signaled, use the Google AI plugin (\`@genkit-ai/google-genai\` for Node, \`github.com/firebase/genkit/go/plugins/googlegenai\` for Go). +- ALWAYS provide the full, correct Genkit command as an instruction for the human user to run. Do not run Genkit commands yourself. +- Do NOT modify parts of the project unrelated to Genkit initialization. +- Respect the user's existing tooling (package manager, language version, project structure). +- Check if Genkit CLI is already installed before recommending installation.`; + +const NODE_SETUP = ` +## Node.js Setup +If the user wants to use Node.js: + +### Project Initialization +- If the directory is empty: + Initialize a new project: + \`\`\`bash + npm init -y + npm install -D typescript tsx @types/node + \`\`\` +- If the directory is not empty (existing project): + - Adhere to the current project structure. + - Detect the package manager in use (npm, pnpm, yarn, bun) and use the corresponding commands. + - Detect if the project is ESM (\`"type": "module"\` in package.json) or CJS. + - For ESM: Use \`import\` syntax. + - For CJS: Use \`require\` syntax. + - IMPORTANT: Do NOT refactor the project (e.g., converting to TypeScript or ESM) solely for Genkit. Work with the existing setup. + +### Dependencies +Install core dependencies (adjust command for the user's package manager): +\`\`\`bash +npm install genkit @genkit-ai/google-genai +\`\`\` +(Add other plugins as requested) + +### Genkit CLI +If the Genkit CLI is not already installed: +\`\`\`bash +npm install -g genkit-cli +\`\`\` + +### Configuration +Create a single \`src/index.ts\` (or \`src/index.js\` for JS) file. + +\`\`\`ts +// src/index.ts +import { genkit, z } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; + +export const ai = genkit({ + plugins: [googleAI()], +}); +\`\`\``; + +const GO_SETUP = ` +## Go Setup +If the user wants to use Go: + +### Project Initialization +- If the directory is empty: + \`\`\`bash + go mod init + \`\`\` +- If the directory is not empty: + Adhere to the current project structure. + +### Dependencies +\`\`\`bash +go get github.com/firebase/genkit/go/genkit +go get github.com/firebase/genkit/go/plugins/googlegenai +go get github.com/firebase/genkit/go/ai +go get google.golang.org/genai +\`\`\` + +### Genkit CLI +If the Genkit CLI is not already installed: +\`\`\`bash +curl -sL cli.genkit.dev | bash +\`\`\` + +### Configuration +Create a \`main.go\` file: + +\`\`\`go +package main + +import ( + "context" + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/googlegenai" +) + +func main() { + ctx := context.Background() + g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) + // Your flows and logic here + <-ctx.Done() +} +\`\`\``; + +const RUNNING_THE_PROJECT = ` +## Running the Project +After setting up the project: +1. Identify the command to run the project's runtime (e.g., \`npm run dev\`, \`go run main.go\`). +2. Use the \`start_runtime\` tool to start the runtime process. This is required for Genkit to discover flows. + - Example: If the project uses \`npm run dev\`, call \`start_runtime\` with \`{ command: "npm", args: ["run", "dev"] }\`. +3. After starting the runtime, instruct the user to run \`genkit start\` in their terminal to launch the Developer UI. +`; + +export function defineInitPrompt(server: McpServer) { + server.registerPrompt( + 'genkit:init', + { + title: 'Initialize Genkit', + description: 'Initializes a new Genkit project', + argsSchema: { + lang: z.enum(['js', 'go']).optional(), + }, + }, + ({ lang }) => { + let content = `You are a Genkit expert. Help the user initialize a Genkit project. + +Follow these rules based on the user's environment and preference:`; + + content += GENERAL_GUIDELINES; + + if (lang === 'js') { + content += NODE_SETUP; + } else if (lang === 'go') { + content += GO_SETUP; + } else { + content += NODE_SETUP; + content += GO_SETUP; + } + + content += RUNNING_THE_PROJECT; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: content, + }, + }, + ], + }; + } + ); +} diff --git a/genkit-tools/cli/src/mcp/runtime.ts b/genkit-tools/cli/src/mcp/runtime.ts new file mode 100644 index 0000000000..daa52e622d --- /dev/null +++ b/genkit-tools/cli/src/mcp/runtime.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { record } from '@genkit-ai/tools-common/utils'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import { z } from 'zod'; +import { McpRunToolEvent } from './analytics.js'; +import { McpRuntimeManager } from './util.js'; + +export function defineRuntimeTools( + server: McpServer, + manager: McpRuntimeManager +) { + server.registerTool( + 'start_runtime', + { + title: 'Starts a Genkit runtime process', + description: `Use this to start a Genkit runtime process (This is typically the entry point to the users app). Once started, the runtime will be picked up by the \`genkit start\` command to power the Dev UI features like model and flow playgrounds. The inputSchema for this tool matches the function prototype for \`NodeJS.child_process.spawn\`. + + Examples: + {command: 'go', args: ['run', 'main.go']} + {command: 'npm', args: ['run', 'dev']}`, + inputSchema: { + command: z.string(), + args: z.array(z.string()), + }, + }, + async ({ command, args }) => { + await record(new McpRunToolEvent('start_runtime')); + await manager.getManagerWithDevProcess(command, args); + + return { + content: [{ type: 'text', text: `Done.` }], + }; + } + ); + + server.registerTool( + 'kill_runtime', + { + title: 'Kills any existing Genkit runtime process', + description: + 'Use this to stop an existing runtime that was started using the `start_runtime` tool', + }, + async () => { + await record(new McpRunToolEvent('kill_runtime')); + const runtimeManager = await manager.getManager(); + if (!runtimeManager.processManager) { + return { + isError: true, + content: [ + { type: 'text', text: `No runtime process currently running.` }, + ], + }; + } + + await runtimeManager.processManager?.kill(); + return { + content: [{ type: 'text', text: `Done.` }], + }; + } + ); + + server.registerTool( + 'restart_runtime', + { + title: 'Restarts any existing Genkit runtime process', + description: + 'Use this to restart an existing runtime that was started using the `start_runtime` tool', + }, + async () => { + await record(new McpRunToolEvent('restart_runtime')); + const runtimeManager = await manager.getManager(); + if (!runtimeManager.processManager) { + return { + isError: true, + content: [ + { type: 'text', text: `No runtime process currently running.` }, + ], + }; + } + + await runtimeManager.processManager?.restart(); + return { + content: [{ type: 'text', text: `Done.` }], + }; + } + ); +} diff --git a/genkit-tools/cli/src/mcp/server.ts b/genkit-tools/cli/src/mcp/server.ts index 3fbab0f208..6aecf2a218 100644 --- a/genkit-tools/cli/src/mcp/server.ts +++ b/genkit-tools/cli/src/mcp/server.ts @@ -19,9 +19,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { defineDocsTool } from '../mcp/docs'; import { defineFlowTools } from './flows'; +import { defineInitPrompt } from './prompts/init'; +import { defineRuntimeTools } from './runtime'; import { defineTraceTools } from './trace'; import { defineUsageGuideTool } from './usage'; -import { lazyLoadManager } from './util'; +import { McpRuntimeManager } from './util'; export async function startMcpServer(projectRoot: string) { const server = new McpServer({ @@ -29,18 +31,38 @@ export async function startMcpServer(projectRoot: string) { version: '0.0.2', }); - const manager = lazyLoadManager(projectRoot); + const manager = new McpRuntimeManager(projectRoot); await defineDocsTool(server); await defineUsageGuideTool(server); + defineInitPrompt(server); + defineRuntimeTools(server, manager); + defineFlowTools(server, manager); defineTraceTools(server, manager); return new Promise(async (resolve) => { const transport = new StdioServerTransport(); - transport.onclose = () => { + const cleanup = async () => { + try { + await manager.kill(); + } catch (e) { + // ignore + } + resolve(undefined); + process.exit(0); + }; + transport.onclose = async () => { + try { + await manager.kill(); + } catch (e) { + // ignore + } resolve(undefined); + process.exit(0); }; + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); await server.connect(transport); logger.info('Genkit MCP Server running on stdio'); }); diff --git a/genkit-tools/cli/src/mcp/trace.ts b/genkit-tools/cli/src/mcp/trace.ts index 0a3ea5d091..7e8520cf40 100644 --- a/genkit-tools/cli/src/mcp/trace.ts +++ b/genkit-tools/cli/src/mcp/trace.ts @@ -18,11 +18,11 @@ import { record } from '@genkit-ai/tools-common/utils'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import z from 'zod'; import { McpRunToolEvent } from './analytics.js'; -import { lazyLoadManager } from './util.js'; +import { McpRuntimeManager } from './util.js'; export function defineTraceTools( server: McpServer, - manager: ReturnType + manager: McpRuntimeManager ) { server.registerTool( 'get_trace', diff --git a/genkit-tools/cli/src/mcp/util.ts b/genkit-tools/cli/src/mcp/util.ts index 0ced95e63e..f39ac569bf 100644 --- a/genkit-tools/cli/src/mcp/util.ts +++ b/genkit-tools/cli/src/mcp/util.ts @@ -15,17 +15,41 @@ */ import { RuntimeManager } from '@genkit-ai/tools-common/manager'; -import { startManager } from '../utils/manager-utils'; +import { startDevProcessManager, startManager } from '../utils/manager-utils'; -/** Lazy loader for RuntimeManager to defer `.genkit/` creation. */ -export function lazyLoadManager(projectRoot: string) { - let manager: RuntimeManager | undefined; - return { - async getManager() { - if (!manager) { - manager = await startManager(projectRoot, true); - } - return manager; - }, - }; +/** Genkit Runtime manager specifically for the MCP server. Allows lazy + * initialization and dev process manangement. */ +export class McpRuntimeManager { + private manager: RuntimeManager | undefined; + + constructor(private projectRoot: string) {} + + async getManager() { + if (!this.manager) { + this.manager = await startManager( + this.projectRoot, + true /* manageHealth */ + ); + } + return this.manager; + } + + async getManagerWithDevProcess(command: string, args: string[]) { + if (this.manager) { + await this.manager.stop(); + } + const devManager = await startDevProcessManager( + this.projectRoot, + command, + args + ); + this.manager = devManager.manager; + return this.manager; + } + + async kill() { + if (this.manager) { + await this.manager.stop(); + } + } } diff --git a/genkit-tools/common/src/manager/manager.ts b/genkit-tools/common/src/manager/manager.ts index b166095939..82ee715b17 100644 --- a/genkit-tools/common/src/manager/manager.ts +++ b/genkit-tools/common/src/manager/manager.ts @@ -68,6 +68,8 @@ export class RuntimeManager { private filenameToDevUiMap: Record = {}; private idToFileMap: Record = {}; private eventEmitter = new EventEmitter(); + private watchers: chokidar.FSWatcher[] = []; + private healthCheckInterval?: NodeJS.Timeout; private constructor( readonly telemetryServerUrl: string | undefined, @@ -91,7 +93,7 @@ export class RuntimeManager { await manager.setupRuntimesWatcher(); await manager.setupDevUiWatcher(); if (manager.manageHealth) { - setInterval( + manager.healthCheckInterval = setInterval( async () => await manager.performHealthChecks(), HEALTH_CHECK_INTERVAL ); @@ -99,6 +101,19 @@ export class RuntimeManager { return manager; } + /** + * Stops the runtime manager and cleans up resources. + */ + async stop() { + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + } + await Promise.all(this.watchers.map((watcher) => watcher.close())); + if (this.processManager) { + await this.processManager.kill(); + } + } + /** * Lists all active runtimes */ @@ -368,6 +383,7 @@ export class RuntimeManager { persistent: true, ignoreInitial: false, }); + this.watchers.push(watcher); watcher.on('add', (filePath) => this.handleNewRuntime(filePath)); if (this.manageHealth) { watcher.on('unlink', (filePath) => this.handleRemovedRuntime(filePath)); @@ -392,6 +408,7 @@ export class RuntimeManager { persistent: true, ignoreInitial: false, }); + this.watchers.push(watcher); watcher.on('add', (filePath) => this.handleNewDevUi(filePath)); if (this.manageHealth) { watcher.on('unlink', (filePath) => this.handleRemovedDevUi(filePath));