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 { 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');
}