// Copyright 2026 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. package skills import ( "fmt" "sort" "strings" "text/template" "github.com/googleapis/mcp-toolbox/internal/tools" "github.com/googleapis/mcp-toolbox/internal/util/parameters" ) const skillTemplate = `--- name: {{.SkillName}} description: {{.SkillDescription}} --- ## Usage All scripts can be executed using Node.js. Replace ` + "`" + `` + "`" + ` and ` + "`" + `` + "`" + ` with actual values. **Bash:** ` + "`" + `node /scripts/.js '{"": ""}'` + "`" + ` **PowerShell:** ` + "`" + `node /scripts/.js '{\"\": \"\"}'` + "`" + ` {{if .AdditionalNotes}} {{.AdditionalNotes}} {{end}} ## Scripts {{range .Tools}} ### {{.Name}} {{.Description}} {{.ParametersSchema}} --- {{end}} ` type toolTemplateData struct { Name string Description string ParametersSchema string } type skillTemplateData struct { SkillName string SkillDescription string AdditionalNotes string Tools []toolTemplateData } // generateSkillMarkdown generates the content of the SKILL.md file. // It includes usage instructions and a reference section for each tool in the skill, // detailing its description and parameters. func generateSkillMarkdown(skillName, skillDescription, additionalNotes string, toolsMap map[string]tools.Tool, envVars map[string]string) (string, error) { var toolsData []toolTemplateData // Order tools based on name var toolNames []string for name := range toolsMap { toolNames = append(toolNames, name) } sort.Strings(toolNames) for _, name := range toolNames { tool := toolsMap[name] manifest := tool.StaticManifest() parametersSchema, err := formatParameters(manifest.Parameters, envVars) if err != nil { return "", err } toolsData = append(toolsData, toolTemplateData{ Name: name, Description: manifest.Description, ParametersSchema: parametersSchema, }) } data := skillTemplateData{ SkillName: skillName, SkillDescription: skillDescription, AdditionalNotes: additionalNotes, Tools: toolsData, } tmpl, err := template.New("markdown").Parse(skillTemplate) if err != nil { return "", fmt.Errorf("error parsing markdown template: %w", err) } var buf strings.Builder if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("error executing markdown template: %w", err) } return buf.String(), nil } const nodeScriptTemplate = `#!/usr/bin/env node {{if .LicenseHeader}} {{.LicenseHeader}} {{end}} const { spawn, execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); const toolName = "{{.Name}}"; const configArgs = [{{.ConfigArgs}}]; {{if .OptionalVars}} const OPTIONAL_VARS_TO_OMIT_IF_EMPTY = [ {{range .OptionalVars}} '{{.}}', {{end}}]; {{end}} function mergeEnvVars(env) { if (process.env.GEMINI_CLI === '1') { const envPath = path.resolve(__dirname, '../../../.env'); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf-8'); envContent.split('\n').forEach(line => { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const splitIdx = trimmed.indexOf('='); if (splitIdx !== -1) { const key = trimmed.slice(0, splitIdx).trim(); let value = trimmed.slice(splitIdx + 1).trim(); value = value.replace(/(^['"]|['"]$)/g, ''); if (env[key] === undefined) { env[key] = value; } } } }); } } else if (process.env.CLAUDECODE === '1') { const prefix = 'CLAUDE_PLUGIN_OPTION_'; for (const key in process.env) { if (key.startsWith(prefix)) { env[key.substring(prefix.length)] = process.env[key]; } } } } function prepareEnvironment() { let env = { ...process.env }; let userAgent = "skills"; if (process.env.GEMINI_CLI === '1') { userAgent = "skills-geminicli"; } else if (process.env.CLAUDECODE === '1') { userAgent = "skills-claudecode"; } else if (process.env.CODEX_CI === '1') { userAgent = "skills-codex"; } mergeEnvVars(env); {{if .OptionalVars}} OPTIONAL_VARS_TO_OMIT_IF_EMPTY.forEach(varName => { if (env[varName] === '') { delete env[varName]; } }); {{end}} return { env, userAgent }; } function main() { const { env, userAgent } = prepareEnvironment(); const args = process.argv.slice(2); {{if eq .InvocationMode "npx"}} const command = os.platform() === 'win32' ? 'npx.cmd' : 'npx'; const processedArgs = os.platform() === 'win32' ? args.map(arg => arg.includes('"') ? '"' + arg.replace(/"/g, '""') + '"' : arg) : args; const npxArgs = ["--yes", "@toolbox-sdk/server@{{.ToolboxVersion}}", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...processedArgs]; const child = spawn(command, npxArgs, { shell: os.platform() === 'win32', stdio: 'inherit', env }); {{else}} function getToolboxPath() { if (process.env.GEMINI_CLI === '1') { const ext = process.platform === 'win32' ? '.exe' : ''; const localPath = path.resolve(__dirname, '../../../toolbox' + ext); if (fs.existsSync(localPath)) { return localPath; } } try { const checkCommand = process.platform === 'win32' ? 'where toolbox' : 'which toolbox'; const globalPath = execSync(checkCommand, { stdio: 'pipe', encoding: 'utf-8' }).trim(); if (globalPath) { return globalPath.split('\n')[0].trim(); } throw new Error("Toolbox binary not found"); } catch (e) { throw new Error("Toolbox binary not found"); } } let toolboxBinary; try { toolboxBinary = getToolboxPath(); } catch (err) { console.error("Error:", err.message); process.exit(1); } const toolboxArgs = ["--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args]; const child = spawn(toolboxBinary, toolboxArgs, { stdio: 'inherit', env }); {{end}} child.on('close', (code) => { process.exit(code); }); child.on('error', (err) => { console.error("Error executing toolbox:", err); process.exit(1); }); } main(); ` type scriptData struct { Name string ConfigArgs string LicenseHeader string InvocationMode string ToolboxVersion string OptionalVars []string } // generateScriptContent creates the content for a Node.js wrapper script. // This script invokes the toolbox CLI with the appropriate configuration // (using a generated config) and arguments to execute the specific tool. func generateScriptContent(name string, configArgs string, licenseHeader string, mode string, version string, optionalVars []string) (string, error) { data := scriptData{ Name: name, ConfigArgs: configArgs, LicenseHeader: licenseHeader, InvocationMode: mode, ToolboxVersion: version, OptionalVars: optionalVars, } tmpl, err := template.New("script").Parse(nodeScriptTemplate) if err != nil { return "", fmt.Errorf("error parsing script template: %w", err) } var buf strings.Builder if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("error executing script template: %w", err) } return buf.String(), nil } // formatParameters converts a list of parameter manifests into a formatted JSON schema string. // This schema is used in the skill documentation to describe the input parameters for a tool. func formatParameters(params []parameters.ParameterManifest, envVars map[string]string) (string, error) { if len(params) == 0 { return "", nil } var sb strings.Builder sb.WriteString("#### Parameters\n\n") sb.WriteString("| Name | Type | Description | Required | Default |\n") sb.WriteString("| :--- | :--- | :--- | :--- | :--- |\n") for _, p := range params { required := "No" if p.Required { required = "Yes" } defaultValue := "" if p.Default != nil { defaultValue = fmt.Sprintf("`%v`", p.Default) // Check if default value matches any env var if strVal, ok := p.Default.(string); ok { for _, envVal := range envVars { if envVal == strVal { defaultValue = "" break } } } } fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s |\n", p.Name, p.Type, p.Description, required, defaultValue) } return sb.String(), nil }