MCP Terminal Server
by dillip285
/**
* 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 * as fs from 'fs/promises';
import * as path from 'path';
import { Runtime } from '../manager/types';
/**
* Finds the project root by looking for a `package.json` file.
*/
export async function findProjectRoot(): Promise<string> {
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const packageJsonPath = path.join(currentDir, 'package.json');
const goModPath = path.join(currentDir, 'go.mod');
const pyprojectPath = path.join(currentDir, 'pyproject.toml');
const pyproject2Path = path.join(currentDir, 'requirements.txt');
try {
const [
packageJsonExists,
goModExists,
pyprojectExists,
pyproject2Exists,
] = await Promise.all([
fs
.access(packageJsonPath)
.then(() => true)
.catch(() => false),
fs
.access(goModPath)
.then(() => true)
.catch(() => false),
fs
.access(pyprojectPath)
.then(() => true)
.catch(() => false),
fs
.access(pyproject2Path)
.then(() => true)
.catch(() => false),
]);
if (
packageJsonExists ||
goModExists ||
pyprojectExists ||
pyproject2Exists
) {
return currentDir;
}
} catch {
// Continue searching if any errors occur
}
currentDir = path.dirname(currentDir);
}
throw new Error('Could not find project root');
}
/**
* Finds the Genkit hidden directory containing runtime state files.
*/
export async function findRuntimesDir(projectRoot?: string): Promise<string> {
const root = projectRoot ?? (await findProjectRoot());
return path.join(root, '.genkit', 'runtimes');
}
/**
* Finds the Genkit hidden directory containing server (UI server, telemetry server, etc) state files.
*/
export async function findServersDir(projectRoot?: string): Promise<string> {
const root = projectRoot ?? (await findProjectRoot());
return path.join(root, '.genkit', 'servers');
}
/**
* Extract project name (i.e. basename of the project root folder) from the
* path to a runtime file.
*
* e.g.) /path/to/<basename>/.genkit/runtimes/123.json returns <basename>.
*
* @param filePath path to a runtime file
* @returns project name
*/
export function projectNameFromGenkitFilePath(filePath: string): string {
const parts = filePath.split('/');
const basePath = parts
.slice(
0,
Math.max(
parts.findIndex((value) => value === '.genkit'),
0
)
)
.join('/');
return basePath === '' ? 'unknown' : path.basename(basePath);
}
/**
* Detects what runtime is used in the current directory.
* @returns Runtime of the project directory.
*/
export async function detectRuntime(directory: string): Promise<Runtime> {
const files = await fs.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = await fs.stat(filePath);
if (stat.isFile() && (path.extname(file) === '.go' || file === 'go.mod')) {
return 'go';
}
}
try {
await fs.access(path.join(directory, 'package.json'));
return 'nodejs';
} catch {
return undefined;
}
}
/**
* Checks the health of a server with a /api/__health endpoint.
*/
export async function checkServerHealth(url: string): Promise<boolean> {
try {
const response = await fetch(`${url}/api/__health`);
return response.status === 200;
} catch (error) {
if (
error instanceof Error &&
(error.cause as any).code === 'ECONNREFUSED'
) {
return false;
}
}
return true;
}
/**
* Waits until the server is healthy or the timeout is reached.
*/
export async function waitUntilHealthy(
url: string,
maxTimeout = 10000
): Promise<boolean> {
const startTime = Date.now();
while (Date.now() - startTime < maxTimeout) {
try {
const response = await fetch(`${url}/api/__health`);
if (response.status === 200) {
return true;
}
} catch (error) {
// Ignore errors and continue retrying
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
/**
* Waits until the server becomes unresponsive or the timeout is reached.
*/
export async function waitUntilUnresponsive(
url: string,
maxTimeout = 10000
): Promise<boolean> {
const startTime = Date.now();
while (Date.now() - startTime < maxTimeout) {
try {
const health = await fetch(`${url}/api/__health`);
} catch (error) {
if (
error instanceof Error &&
(error.cause as any).code === 'ECONNREFUSED'
) {
return true;
}
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
export async function retriable<T>(
fn: () => Promise<T>,
opts: { maxRetries?: number; delayMs?: number }
): Promise<T> {
const maxRetries = opts.maxRetries ?? 3;
const delayMs = opts.delayMs ?? 0;
let attempt = 0;
while (true) {
try {
return await fn();
} catch (e) {
if (attempt >= maxRetries - 1) {
throw e;
}
if (delayMs > 0) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
attempt++;
}
}