Skip to content
Merged
7 changes: 4 additions & 3 deletions genkit-tools/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@
"directory": "genkit-tools/cli"
},
"dependencies": {
"@genkit-ai/tools-common": "workspace:*",
"@genkit-ai/telemetry-server": "workspace:*",
"@genkit-ai/tools-common": "workspace:*",
"@modelcontextprotocol/sdk": "^1.13.1",
"axios": "^1.7.7",
"colorette": "^2.0.20",
"commander": "^11.1.0",
"extract-zip": "^2.0.1",
"open": "^6.3.0",
"inquirer": "^8.2.0",
"get-port": "5.1.1",
"inquirer": "^8.2.0",
"open": "^6.3.0",
"ora": "^5.4.1"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions genkit-tools/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { evalFlow } from './commands/eval-flow';
import { evalRun } from './commands/eval-run';
import { flowBatchRun } from './commands/flow-batch-run';
import { flowRun } from './commands/flow-run';
import { mcp } from './commands/mcp';
import { getPluginCommands, getPluginSubCommand } from './commands/plugins';
import { start } from './commands/start';
import { uiStart } from './commands/ui-start';
Expand All @@ -50,6 +51,7 @@ const commands: Command[] = [
evalFlow,
config,
start,
mcp,
];

/** Main entry point for CLI. */
Expand Down
5 changes: 3 additions & 2 deletions genkit-tools/cli/src/commands/eval-extract-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
TraceData,
} from '@genkit-ai/tools-common';
import {
findProjectRoot,
generateTestCaseId,
getEvalExtractors,
logger,
Expand All @@ -45,7 +46,7 @@ export const evalExtractData = new Command('eval:extractData')
.option('--maxRows <maxRows>', 'maximum number of rows', '100')
.option('--label [label]', 'label flow run in this batch')
.action(async (flowName: string, options: EvalDatasetOptions) => {
await runWithManager(async (manager) => {
await runWithManager(await findProjectRoot(), async (manager) => {
const extractors = await getEvalExtractors(`/flow/${flowName}`);

logger.info(`Extracting trace data '/flow/${flowName}'...`);
Expand Down Expand Up @@ -105,7 +106,7 @@ export const evalExtractData = new Command('eval:extractData')
);
} else {
logger.info(`Results will not be written to file.`);
console.log(`Results: ${JSON.stringify(dataset, undefined, ' ')}`);
logger.info(`Results: ${JSON.stringify(dataset, undefined, ' ')}`);
}
});
});
Expand Down
3 changes: 2 additions & 1 deletion genkit-tools/cli/src/commands/eval-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@genkit-ai/tools-common/eval';
import {
confirmLlmUse,
findProjectRoot,
hasAction,
loadInferenceDatasetFile,
logger,
Expand Down Expand Up @@ -90,7 +91,7 @@ export const evalFlow = new Command('eval:flow')
.option('-f, --force', 'Automatically accept all interactive prompts')
.action(
async (flowName: string, data: string, options: EvalFlowRunCliOptions) => {
await runWithManager(async (manager) => {
await runWithManager(await findProjectRoot(), async (manager) => {
const actionRef = `/flow/${flowName}`;
if (!data && !options.input) {
throw new Error(
Expand Down
3 changes: 2 additions & 1 deletion genkit-tools/cli/src/commands/eval-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@genkit-ai/tools-common/eval';
import {
confirmLlmUse,
findProjectRoot,
loadEvaluationDatasetFile,
logger,
} from '@genkit-ai/tools-common/utils';
Expand Down Expand Up @@ -66,7 +67,7 @@ export const evalRun = new Command('eval:run')
)
.option('--force', 'Automatically accept all interactive prompts')
.action(async (dataset: string, options: EvalRunCliOptions) => {
await runWithManager(async (manager) => {
await runWithManager(await findProjectRoot(), async (manager) => {
if (!dataset) {
throw new Error(
'No input data passed. Specify input data using [data] argument'
Expand Down
4 changes: 2 additions & 2 deletions genkit-tools/cli/src/commands/flow-batch-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { logger } from '@genkit-ai/tools-common/utils';
import { findProjectRoot, logger } from '@genkit-ai/tools-common/utils';
import { Command } from 'commander';
import { readFile, writeFile } from 'fs/promises';
import { runWithManager } from '../utils/manager-utils';
Expand Down Expand Up @@ -43,7 +43,7 @@ export const flowBatchRun = new Command('flow:batchRun')
fileName: string,
options: FlowBatchRunOptions
) => {
await runWithManager(async (manager) => {
await runWithManager(await findProjectRoot(), async (manager) => {
const inputData = JSON.parse(await readFile(fileName, 'utf8')) as any[];
let input = inputData;
if (inputData.length === 0) {
Expand Down
4 changes: 2 additions & 2 deletions genkit-tools/cli/src/commands/flow-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { logger } from '@genkit-ai/tools-common/utils';
import { findProjectRoot, logger } from '@genkit-ai/tools-common/utils';
import { Command } from 'commander';
import { writeFile } from 'fs/promises';
import { runWithManager } from '../utils/manager-utils';
Expand All @@ -39,7 +39,7 @@ export const flowRun = new Command('flow:run')
'name of the output file to store the extracted data'
)
.action(async (flowName: string, data: string, options: FlowRunOptions) => {
await runWithManager(async (manager) => {
await runWithManager(await findProjectRoot(), async (manager) => {
logger.info(`Running '/flow/${flowName}' (stream=${options.stream})...`);
const result = (
await manager.runAction(
Expand Down
37 changes: 37 additions & 0 deletions genkit-tools/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright 2024 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 { findProjectRoot, forceStderr } from '@genkit-ai/tools-common/utils';
import { Command } from 'commander';
import { startMcpServer } from '../mcp/server';
import { startManager } from '../utils/manager-utils';

interface McpOptions {
projectRoot?: string;
}

/** Command to run MCP server. */
export const mcp = new Command('mcp')
.option('--project-root [projectRoot]', 'Project root')
.description('run MCP stdio server (EXPERIMENTAL, subject to change)')
.action(async (options: McpOptions) => {
forceStderr();
const manager = await startManager(
options.projectRoot ?? (await findProjectRoot()),
true
);
await startMcpServer(manager);
});
9 changes: 6 additions & 3 deletions genkit-tools/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import type { RuntimeManager } from '@genkit-ai/tools-common/manager';
import { startServer } from '@genkit-ai/tools-common/server';
import { logger } from '@genkit-ai/tools-common/utils';
import { findProjectRoot, logger } from '@genkit-ai/tools-common/utils';
import { spawn } from 'child_process';
import { Command } from 'commander';
import getPort, { makeRange } from 'get-port';
Expand All @@ -37,7 +37,10 @@ export const start = new Command('start')
.option('-o, --open', 'Open the browser on UI start up')
.action(async (options: RunOptions) => {
// Always start the manager.
let managerPromise: Promise<RuntimeManager> = startManager(true);
let managerPromise: Promise<RuntimeManager> = startManager(
await findProjectRoot(),
true
);
if (!options.noui) {
let port: number;
if (options.port) {
Expand Down Expand Up @@ -81,7 +84,7 @@ async function startRuntime(telemetryServerUrl?: string) {
process.stdin?.pipe(appProcess.stdin);

appProcess.on('error', (error): void => {
console.log(`Error in app process: ${error}`);
logger.error(`Error in app process: ${error}`);
reject(error);
process.exitCode = 1;
});
Expand Down
3 changes: 2 additions & 1 deletion genkit-tools/cli/src/commands/ui-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import {
findProjectRoot,
findServersDir,
isValidDevToolsInfo,
logger,
Expand Down Expand Up @@ -53,7 +54,7 @@ export const uiStart = new Command('ui:start')
} else {
port = await getPort({ port: makeRange(4000, 4099) });
}
const serversDir = await findServersDir();
const serversDir = await findServersDir(await findProjectRoot());
const toolsJsonPath = path.join(serversDir, 'tools.json');
try {
const toolsJsonContent = await fs.readFile(toolsJsonPath, 'utf-8');
Expand Down
3 changes: 2 additions & 1 deletion genkit-tools/cli/src/commands/ui-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import {
findProjectRoot,
findServersDir,
isValidDevToolsInfo,
logger,
Expand All @@ -31,7 +32,7 @@ import path from 'path';
export const uiStop = new Command('ui:stop')
.description('stops any running Genkit Developer UI in this directory')
.action(async () => {
const serversDir = await findServersDir();
const serversDir = await findServersDir(await findProjectRoot());
const toolsJsonPath = path.join(serversDir, 'tools.json');
try {
const toolsJsonContent = await fs.readFile(toolsJsonPath, 'utf-8');
Expand Down
129 changes: 129 additions & 0 deletions genkit-tools/cli/src/mcp/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* 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 { ContentBlock } from '@modelcontextprotocol/sdk/types';
import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';
import os from 'os';
import path from 'path';
import z from 'zod';
import { version } from '../utils/version';

const DOCS_URL =
process.env.GENKIT_DOCS_BUNDLE_URL ??
'http://genkit.dev/docs-bundle-experimental.json';

const DOCS_BUNDLE_FILE_PATH = path.resolve(
os.homedir(),
'.genkit',
'docs',
version,
'bundle.json'
);

async function maybeDownloadDocsBundle() {
if (existsSync(DOCS_BUNDLE_FILE_PATH)) {
return;
}
const response = await fetch(DOCS_URL);
if (response.status !== 200) {
throw new Error(
'Failed to download genkit docs bundle. Try again later or/and report the issue.\n\n' +
DOCS_URL
);
}
const stream = Readable.fromWeb(response.body as any);

mkdirSync(path.dirname(DOCS_BUNDLE_FILE_PATH), { recursive: true });

await writeFile(DOCS_BUNDLE_FILE_PATH + '.pending', stream);
renameSync(DOCS_BUNDLE_FILE_PATH + '.pending', DOCS_BUNDLE_FILE_PATH);
}

interface Doc {
title: string;
description?: string;
text: string;
lang: string;
headers: string;
}

export async function defineDocsTool(server: McpServer) {
await maybeDownloadDocsBundle();
const documents = JSON.parse(
readFileSync(DOCS_BUNDLE_FILE_PATH, { encoding: 'utf8' })
) as Record<string, Doc>;

server.registerTool(
'lookup_genkit_docs',
{
title: 'Genkit Docs',
description:
'Use this to look up documentation for the Genkit AI framework.',
inputSchema: {
language: z
.enum(['js', 'go', 'python'])
.describe('which language these docs are for (default js).')
.default('js'),
files: z
.array(z.string())
.describe(
'Specific docs files to look up. If empty or not specified an index will be returned. Always lookup index first for exact file names.'
)
.optional(),
},
},
async ({ language, files }) => {
const content = [] as ContentBlock[];
if (!language) {
language = 'js';
}

if (!files || !files.length) {
content.push({
type: 'text',
text:
Object.keys(documents)
.filter((file) => file.startsWith(language))
.map((file) => {
let fileSummary = ` - File: ${file}\n Title: ${documents[file].title}\n`;
if (documents[file].description) {
fileSummary += ` Description: ${documents[file].description}\n`;
}
if (documents[file].headers) {
fileSummary += ` Headers:\n ${documents[file].headers.split('\n').join('\n ')}\n`;
}
return fileSummary;
})
.join('\n') +
`\n\nIMPORTANT: if doing anything more than basic model calling, look up "${language}/models.md" file, it contains important details about how to work with models.\n\n`,
});
} else {
for (const file of files) {
if (documents[file]) {
content.push({ type: 'text', text: documents[file]?.text });
} else {
content.push({ type: 'text', text: `${file} not found` });
}
}
}

return { content };
}
);
}
Loading
Loading