Skip to main content
Glama

Genkit MCP

Official
by firebase
converters.ts12.6 kB
/** * 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 { GenkitError, z } from 'genkit'; import { CandidateData, MessageData, ModelReference, Part, ToolDefinition, } from 'genkit/model'; import { FunctionCallingMode, FunctionDeclaration, GenerateContentCandidate as GeminiCandidate, Content as GeminiContent, Part as GeminiPart, Schema, SchemaType, VideoMetadata, } from './types.js'; export function toGeminiTool(tool: ToolDefinition): FunctionDeclaration { const declaration: FunctionDeclaration = { name: tool.name.replace(/\//g, '__'), // Gemini throws on '/' in tool name description: tool.description, parameters: toGeminiSchemaProperty(tool.inputSchema), }; return declaration; } function toGeminiSchemaProperty(property?: ToolDefinition['inputSchema']) { if (!property || !property.type) { return undefined; } const baseSchema: Schema = {}; if (property.description) { baseSchema.description = property.description; } if (property.enum) { baseSchema.enum = property.enum; } if (property.nullable) { baseSchema.nullable = property.nullable; } let propertyType; // nullable schema can ALSO be defined as, for example, type=['string','null'] if (Array.isArray(property.type)) { const types = property.type as string[]; if (types.includes('null')) { baseSchema.nullable = true; } // grab the type that's not `null` propertyType = types.find((t) => t !== 'null'); } else { propertyType = property.type; } if (propertyType === 'object') { const nestedProperties = {}; Object.keys(property.properties).forEach((key) => { nestedProperties[key] = toGeminiSchemaProperty(property.properties[key]); }); return { ...baseSchema, type: SchemaType.OBJECT, properties: nestedProperties, required: property.required, }; } else if (propertyType === 'array') { return { ...baseSchema, type: SchemaType.ARRAY, items: toGeminiSchemaProperty(property.items), }; } else { const schemaType = SchemaType[propertyType.toUpperCase()] as SchemaType; if (!schemaType) { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: `Unsupported property type ${propertyType.toUpperCase()}`, }); } return { ...baseSchema, type: schemaType, }; } } function toGeminiMedia(part: Part): GeminiPart { let media: GeminiPart; if (part.media?.url.startsWith('data:')) { // Inline data const dataUrl = part.media.url; const b64Data = dataUrl.substring(dataUrl.indexOf(',')! + 1); const contentType = part.media.contentType || dataUrl.substring(dataUrl.indexOf(':')! + 1, dataUrl.indexOf(';')); media = { inlineData: { mimeType: contentType, data: b64Data } }; } else { // File data if (!part.media?.contentType) { throw Error( 'Must supply a `contentType` when sending File URIs to Gemini.' ); } media = { fileData: { mimeType: part.media.contentType, fileUri: part.media.url, }, }; } // Video metadata if (part.metadata?.videoMetadata) { let videoMetadata = part.metadata.videoMetadata as VideoMetadata; media.videoMetadata = { ...videoMetadata }; } return media; } function toGeminiToolRequest(part: Part): GeminiPart { if (!part.toolRequest?.input) { throw Error('Invalid ToolRequestPart: input was missing.'); } return { functionCall: { name: part.toolRequest.name, args: part.toolRequest.input, }, }; } function toGeminiToolResponse(part: Part): GeminiPart { if (!part.toolResponse?.output) { throw Error('Invalid ToolResponsePart: output was missing.'); } return { functionResponse: { name: part.toolResponse.name, response: { name: part.toolResponse.name, content: part.toolResponse.output, }, }, }; } function toGeminiReasoning(part: Part): GeminiPart { const out: GeminiPart = { thought: true }; if (typeof part.metadata?.thoughtSignature === 'string') { out.thoughtSignature = part.metadata.thoughtSignature; } if (part.reasoning?.length) { out.text = part.reasoning; } return out; } function toGeminiCustom(part: Part): GeminiPart { if (part.custom?.codeExecutionResult) { return { codeExecutionResult: part.custom.codeExecutionResult, }; } if (part.custom?.executableCode) { return { executableCode: part.custom.executableCode, }; } throw new Error('Unsupported Custom Part type'); } function toGeminiPart(part: Part): GeminiPart { if (part.text) { return { text: part.text }; } if (part.media) { return toGeminiMedia(part); } if (part.toolRequest) { return toGeminiToolRequest(part); } if (part.toolResponse) { return toGeminiToolResponse(part); } if (typeof part.reasoning === 'string') { return toGeminiReasoning(part); } if (part.custom) { return toGeminiCustom(part); } throw new Error('Unsupported Part type ' + JSON.stringify(part)); } function toGeminiRole( role: MessageData['role'], model?: ModelReference<z.ZodTypeAny> ): string { switch (role) { case 'user': return 'user'; case 'model': return 'model'; case 'system': if (model?.info?.supports?.systemRole) { // We should have already pulled out the supported system messages, // anything remaining is unsupported; throw an error. throw new Error( 'system role is only supported for a single message in the first position' ); } else { throw new Error('system role is not supported'); } case 'tool': return 'function'; default: return 'user'; } } export function toGeminiMessage( message: MessageData, model?: ModelReference<z.ZodTypeAny> ): GeminiContent { let sortedParts = message.content; if (message.role === 'tool') { sortedParts = [...message.content].sort((a, b) => { const aRef = a.toolResponse?.ref; const bRef = b.toolResponse?.ref; if (!aRef && !bRef) return 0; if (!aRef) return 1; if (!bRef) return -1; return parseInt(aRef, 10) - parseInt(bRef, 10); }); } return { role: toGeminiRole(message.role, model), parts: sortedParts.map(toGeminiPart), }; } export function toGeminiSystemInstruction(message: MessageData): GeminiContent { return { role: 'user', parts: message.content.map(toGeminiPart), }; } /** * Converts mode from either genkit tool choice (lowercase) * or functionCallingConfig (uppercase). * @param from The mode to convert from * @returns */ export function toGeminiFunctionModeEnum( from?: string //genkitMode: 'auto' | 'required' | 'none' ): FunctionCallingMode | undefined { if (from === undefined) { return undefined; } switch (from) { case 'MODE_UNSPECIFIED': { return FunctionCallingMode.MODE_UNSPECIFIED; } case 'required': case 'ANY': { return FunctionCallingMode.ANY; } case 'auto': case 'AUTO': { return FunctionCallingMode.AUTO; } case 'none': case 'NONE': { return FunctionCallingMode.NONE; } default: throw new Error(`unsupported function calling mode: ${from}`); } } function fromGeminiFinishReason( reason: GeminiCandidate['finishReason'] ): CandidateData['finishReason'] { if (!reason) return 'unknown'; switch (reason) { case 'STOP': return 'stop'; case 'MAX_TOKENS': return 'length'; case 'SAFETY': // blocked for safety case 'RECITATION': // blocked for reciting training data case 'LANGUAGE': // blocked for using an unsupported language case 'BLOCKLIST': // blocked for forbidden terms case 'PROHIBITED_CONTENT': // blocked for potentially containing prohibited content case 'SPII': // blocked for potentially containing Sensitive Personally Identifiable Information return 'blocked'; case 'MALFORMED_FUNCTION_CALL': case 'OTHER': return 'other'; default: return 'unknown'; } } function fromGeminiThought(part: GeminiPart): Part { return { reasoning: part.text || '', metadata: { thoughtSignature: part.thoughtSignature }, }; } function fromGeminiInlineData(part: GeminiPart): Part { // Check if the required properties exist if ( !part.inlineData || !part.inlineData.hasOwnProperty('mimeType') || !part.inlineData.hasOwnProperty('data') ) { throw new Error('Invalid GeminiPart: missing required properties'); } const { mimeType, data } = part.inlineData; // Combine data and mimeType into a data URL const dataUrl = `data:${mimeType};base64,${data}`; return { media: { url: dataUrl, contentType: mimeType, }, }; } function fromGeminiFileData(part: GeminiPart): Part { if ( !part.fileData || !part.fileData.hasOwnProperty('mimeType') || !part.fileData.hasOwnProperty('fileUri') ) { throw new Error( 'Invalid Gemini File Data Part: missing required properties' ); } return { media: { url: part.fileData?.fileUri, contentType: part.fileData?.mimeType, }, }; } function fromGeminiFunctionCall(part: GeminiPart, ref: string): Part { if (!part.functionCall) { throw Error( 'Invalid Gemini Function Call Part: missing function call data' ); } return { toolRequest: { name: part.functionCall.name, input: part.functionCall.args, ref, }, }; } function fromGeminiFunctionResponse(part: GeminiPart, ref?: string): Part { if (!part.functionResponse) { throw new Error( 'Invalid Gemini Function Call Part: missing function call data' ); } return { toolResponse: { name: part.functionResponse.name.replace(/__/g, '/'), // restore slashes output: part.functionResponse.response, ref, }, }; } function fromExecutableCode(part: GeminiPart): Part { if (!part.executableCode) { throw new Error('Invalid GeminiPart: missing executableCode'); } return { custom: { executableCode: { language: part.executableCode.language, code: part.executableCode.code, }, }, }; } function fromCodeExecutionResult(part: GeminiPart): Part { if (!part.codeExecutionResult) { throw new Error('Invalid GeminiPart: missing codeExecutionResult'); } return { custom: { codeExecutionResult: { outcome: part.codeExecutionResult.outcome, output: part.codeExecutionResult.output, }, }, }; } function fromGeminiPart(part: GeminiPart, ref: string): Part { if (part.thought) return fromGeminiThought(part as any); if (typeof part.text === 'string') return { text: part.text }; if (part.inlineData) return fromGeminiInlineData(part); if (part.fileData) return fromGeminiFileData(part); if (part.functionCall) return fromGeminiFunctionCall(part, ref); if (part.functionResponse) return fromGeminiFunctionResponse(part, ref); if (part.executableCode) return fromExecutableCode(part); if (part.codeExecutionResult) return fromCodeExecutionResult(part); throw new Error('Unsupported GeminiPart type ' + JSON.stringify(part)); } export function fromGeminiCandidate(candidate: GeminiCandidate): CandidateData { const parts = candidate.content?.parts || []; const genkitCandidate: CandidateData = { index: candidate.index || 0, message: { role: 'model', content: parts // the model sometimes returns empty parts, ignore those. .filter((p) => Object.keys(p).length > 0) .map((part, index) => fromGeminiPart(part, index.toString())), }, finishReason: fromGeminiFinishReason(candidate.finishReason), finishMessage: candidate.finishMessage, custom: { safetyRatings: candidate.safetyRatings, citationMetadata: candidate.citationMetadata, }, }; return genkitCandidate; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/firebase/genkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server