MCP Terminal Server

/** * 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 { TraceFlags } from '@opentelemetry/api'; import { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; import { resolveCurrentPrincipal } from './auth.js'; /** * The maximum length (in characters) of a logged input or output. * This limit exists to align the logs with GCP logging size limits. * */ const MAX_LOG_CONTENT_CHARS = 128_000; /** * The maximum length (in characters) of a flow path. */ const MAX_PATH_CHARS = 4096; export function extractOuterFlowNameFromPath(path: string) { if (!path || path === '<unknown>') { return '<unknown>'; } const flowName = path.match('/{(.+),t:flow}+'); return flowName ? flowName[1] : '<unknown>'; } export function truncate( text: string, limit: number = MAX_LOG_CONTENT_CHARS ): string { return text ? text.substring(0, limit) : text; } export function truncatePath(path: string) { return truncate(path, MAX_PATH_CHARS); } /** * Extract first feature name from a path * e.g. for /{myFlow,t:flow}/{myStep,t:flowStep}/{googleai/gemini-pro,t:action,s:model} * returns "myFlow" */ export function extractOuterFeatureNameFromPath(path: string) { if (!path || path === '<unknown>') { return '<unknown>'; } const first = path.split('/')[1]; const featureName = first?.match('{(.+),t:(flow|action|prompt|helper)'); return featureName ? featureName[1] : '<unknown>'; } export function extractErrorName(events: TimedEvent[]): string | undefined { return events .filter((event) => event.name === 'exception') .map((event) => { const attributes = event.attributes; return attributes ? truncate(attributes['exception.type'] as string, 1024) : '<unknown>'; }) .at(0); } export function extractErrorMessage(events: TimedEvent[]): string | undefined { return events .filter((event) => event.name === 'exception') .map((event) => { const attributes = event.attributes; return attributes ? truncate(attributes['exception.message'] as string, 4096) : '<unknown>'; }) .at(0); } export function extractErrorStack(events: TimedEvent[]): string | undefined { return events .filter((event) => event.name === 'exception') .map((event) => { const attributes = event.attributes; return attributes ? truncate(attributes['exception.stacktrace'] as string, 32_768) : '<unknown>'; }) .at(0); } export function createCommonLogAttributes( span: ReadableSpan, projectId?: string ) { const spanContext = span.spanContext(); const isSampled = !!(spanContext.traceFlags & TraceFlags.SAMPLED); return { 'logging.googleapis.com/spanId': spanContext.spanId, 'logging.googleapis.com/trace': `projects/${projectId}/traces/${spanContext.traceId}`, 'logging.googleapis.com/trace_sampled': isSampled ? '1' : '0', }; } export function requestDenied( err: Error & { code?: number; statusDetails?: Record<string, any>[]; } ) { return err.code === 7; } export function loggingDenied( err: Error & { code?: number; statusDetails?: Record<string, any>[]; } ) { return ( requestDenied(err) && err.statusDetails?.some((details) => { return details?.metadata?.permission === 'logging.logEntries.create'; }) ); } export function tracingDenied( err: Error & { code?: number; statusDetails?: Record<string, any>[]; } ) { // Looks like we don't get status details like we do with logging return requestDenied(err); } export function metricsDenied( err: Error & { code?: number; statusDetails?: Record<string, any>[]; } ) { // Looks like we don't get status details like we do with logging return requestDenied(err); } export async function permissionDeniedHelpText(role: string) { const principal = await resolveCurrentPrincipal(); return `Add the role '${role}' to your Service Account in the IAM & Admin page on the Google Cloud console, or use the following command:\n\ngcloud projects add-iam-policy-binding ${principal.projectId ?? '${PROJECT_ID}'} \\\n --member=serviceAccount:${principal.serviceAccountEmail || '${SERVICE_ACCT}'} \\\n --role=${role}`; } export async function loggingDeniedHelpText() { return permissionDeniedHelpText('roles/logging.logWriter'); } export async function tracingDeniedHelpText() { return permissionDeniedHelpText('roles/cloudtrace.agent'); } export async function metricsDeniedHelpText() { return permissionDeniedHelpText('roles/monitoring.metricWriter'); }