mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: auto-generate slash commands from skills
All skills are now automatically available as slash commands by their mere existence - no configuration needed. Skills can be invoked using /skill-name syntax. - Add getSkillsAsCommands() method to SkillsManager - Merge skill commands with regular commands in webviewMessageHandler - Handle skill-based commands in RunSlashCommandTool - Add unit tests for skill-as-command functionality
This commit is contained in:
parent
67e568f6bb
commit
4d097e161d
6 changed files with 362 additions and 161 deletions
|
|
@ -5,6 +5,7 @@ import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
|
|||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
import type { ToolUse } from "../../shared/tools"
|
||||
import { getModeBySlug } from "../../shared/modes"
|
||||
import type { SkillContent } from "../../shared/skills"
|
||||
|
||||
interface RunSlashCommandParams {
|
||||
command: string
|
||||
|
|
@ -49,26 +50,91 @@ export class RunSlashCommandTool extends BaseTool<"run_slash_command"> {
|
|||
// Get the command from the commands service
|
||||
const command = await getCommand(task.cwd, commandName)
|
||||
|
||||
// Check if this might be a skill-based command
|
||||
let skillContent: SkillContent | null = null
|
||||
let skillName: string | undefined
|
||||
|
||||
if (!command) {
|
||||
// Try to find a skill-based command with this name
|
||||
const skillsManager = provider?.getSkillsManager?.()
|
||||
const currentMode = state?.mode ?? "code"
|
||||
|
||||
if (skillsManager) {
|
||||
const skillCommands = skillsManager.getSkillsAsCommands(currentMode)
|
||||
const matchingSkillCmd = skillCommands.find((sc) => sc.name === commandName)
|
||||
|
||||
if (matchingSkillCmd) {
|
||||
// Found a skill-based command - load the skill content
|
||||
skillName = matchingSkillCmd.skillName
|
||||
skillContent = await skillsManager.getSkillContent(skillName, currentMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!command && !skillContent) {
|
||||
// Get available commands for error message
|
||||
const availableCommands = await getCommandNames(task.cwd)
|
||||
|
||||
// Also include skill-based command names
|
||||
const skillsManager = provider?.getSkillsManager?.()
|
||||
const currentMode = state?.mode ?? "code"
|
||||
const skillCommandNames = skillsManager?.getSkillsAsCommands(currentMode).map((sc) => sc.name) ?? []
|
||||
const allCommandNames = [...new Set([...availableCommands, ...skillCommandNames])]
|
||||
|
||||
task.recordToolError("run_slash_command")
|
||||
task.didToolFailInCurrentTurn = true
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`Command '${commandName}' not found. Available commands: ${availableCommands.join(", ") || "(none)"}`,
|
||||
`Command '${commandName}' not found. Available commands: ${allCommandNames.join(", ") || "(none)"}`,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle skill-based command
|
||||
if (skillContent) {
|
||||
const toolMessage = JSON.stringify({
|
||||
tool: "runSlashCommand",
|
||||
command: commandName,
|
||||
args: args,
|
||||
source: skillContent.source,
|
||||
description: skillContent.description,
|
||||
isSkill: true,
|
||||
skillName: skillName,
|
||||
})
|
||||
|
||||
const didApprove = await askApproval("tool", toolMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the result message for skill-based command
|
||||
let result = `Skill Command: /${commandName}`
|
||||
|
||||
if (skillContent.description) {
|
||||
result += `\nDescription: ${skillContent.description}`
|
||||
}
|
||||
|
||||
if (args) {
|
||||
result += `\nProvided arguments: ${args}`
|
||||
}
|
||||
|
||||
result += `\nSource: ${skillContent.source} (skill: ${skillName})`
|
||||
result += `\n\n--- Skill Instructions ---\n\n${skillContent.instructions}`
|
||||
|
||||
pushToolResult(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle regular command
|
||||
const toolMessage = JSON.stringify({
|
||||
tool: "runSlashCommand",
|
||||
command: commandName,
|
||||
args: args,
|
||||
source: command.source,
|
||||
description: command.description,
|
||||
mode: command.mode,
|
||||
source: command!.source,
|
||||
description: command!.description,
|
||||
mode: command!.mode,
|
||||
})
|
||||
|
||||
const didApprove = await askApproval("tool", toolMessage)
|
||||
|
|
@ -78,35 +144,34 @@ export class RunSlashCommandTool extends BaseTool<"run_slash_command"> {
|
|||
}
|
||||
|
||||
// Switch mode if specified in the command frontmatter
|
||||
if (command.mode) {
|
||||
const provider = task.providerRef.deref()
|
||||
const targetMode = getModeBySlug(command.mode, (await provider?.getState())?.customModes)
|
||||
if (command!.mode) {
|
||||
const targetMode = getModeBySlug(command!.mode, (await provider?.getState())?.customModes)
|
||||
if (targetMode) {
|
||||
await provider?.handleModeSwitch(command.mode)
|
||||
await provider?.handleModeSwitch(command!.mode)
|
||||
}
|
||||
}
|
||||
|
||||
// Build the result message
|
||||
let result = `Command: /${commandName}`
|
||||
|
||||
if (command.description) {
|
||||
result += `\nDescription: ${command.description}`
|
||||
if (command!.description) {
|
||||
result += `\nDescription: ${command!.description}`
|
||||
}
|
||||
|
||||
if (command.argumentHint) {
|
||||
result += `\nArgument hint: ${command.argumentHint}`
|
||||
if (command!.argumentHint) {
|
||||
result += `\nArgument hint: ${command!.argumentHint}`
|
||||
}
|
||||
|
||||
if (command.mode) {
|
||||
result += `\nMode: ${command.mode}`
|
||||
if (command!.mode) {
|
||||
result += `\nMode: ${command!.mode}`
|
||||
}
|
||||
|
||||
if (args) {
|
||||
result += `\nProvided arguments: ${args}`
|
||||
}
|
||||
|
||||
result += `\nSource: ${command.source}`
|
||||
result += `\n\n--- Command Content ---\n\n${command.content}`
|
||||
result += `\nSource: ${command!.source}`
|
||||
result += `\n\n--- Command Content ---\n\n${command!.content}`
|
||||
|
||||
// Return the command content as the tool result
|
||||
pushToolResult(result)
|
||||
|
|
|
|||
|
|
@ -2951,9 +2951,43 @@ export const webviewMessageHandler = async (
|
|||
filePath: command.filePath,
|
||||
description: command.description,
|
||||
argumentHint: command.argumentHint,
|
||||
isSkill: false,
|
||||
}))
|
||||
|
||||
await provider.postMessageToWebview({ type: "commands", commands: commandList })
|
||||
// Get skill-based commands
|
||||
const skillsManager = provider.getSkillsManager()
|
||||
const state = await provider.getState()
|
||||
const currentMode = state?.mode ?? "code"
|
||||
const skillCommands = skillsManager?.getSkillsAsCommands(currentMode) ?? []
|
||||
|
||||
// Add skill-based commands (with isSkill flag for differentiation)
|
||||
const skillCommandList = skillCommands.map((skill) => ({
|
||||
name: skill.name,
|
||||
source: skill.source,
|
||||
filePath: skill.skillPath,
|
||||
description: skill.description,
|
||||
argumentHint: undefined as string | undefined,
|
||||
isSkill: true,
|
||||
skillName: skill.skillName,
|
||||
}))
|
||||
|
||||
// Merge, with regular commands taking priority over skill commands
|
||||
const mergedCommands: Array<{
|
||||
name: string
|
||||
source: "global" | "project" | "built-in"
|
||||
filePath: string
|
||||
description: string | undefined
|
||||
argumentHint?: string | undefined
|
||||
isSkill: boolean
|
||||
skillName?: string
|
||||
}> = [...commandList]
|
||||
for (const skillCmd of skillCommandList) {
|
||||
if (!mergedCommands.some((c) => c.name === skillCmd.name)) {
|
||||
mergedCommands.push(skillCmd)
|
||||
}
|
||||
}
|
||||
|
||||
await provider.postMessageToWebview({ type: "commands", commands: mergedCommands })
|
||||
} catch (error) {
|
||||
provider.log(`Error fetching commands: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
|
||||
await provider.postMessageToWebview({ type: "commands", commands: [] })
|
||||
|
|
|
|||
|
|
@ -233,6 +233,33 @@ export class SkillsManager {
|
|||
return Array.from(this.skills.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all skills as slash commands.
|
||||
* All skills are automatically available as slash commands.
|
||||
* Returns them in a format compatible with the Command interface.
|
||||
*
|
||||
* @param currentMode - The current mode slug to filter skills by
|
||||
*/
|
||||
getSkillsAsCommands(currentMode: string): Array<{
|
||||
name: string
|
||||
description: string
|
||||
source: "global" | "project" | "built-in"
|
||||
skillPath: string
|
||||
skillName: string
|
||||
mode?: string
|
||||
}> {
|
||||
const skills = this.getSkillsForMode(currentMode)
|
||||
|
||||
return skills.map((skill) => ({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
source: skill.source,
|
||||
skillPath: skill.path,
|
||||
skillName: skill.name, // Original skill name for invocation
|
||||
mode: skill.mode,
|
||||
}))
|
||||
}
|
||||
|
||||
async getSkillContent(name: string, currentMode?: string): Promise<SkillContent | null> {
|
||||
// If mode is provided, try to find the best matching skill
|
||||
let skill: SkillMetadata | undefined
|
||||
|
|
|
|||
|
|
@ -827,6 +827,74 @@ description: A test skill
|
|||
})
|
||||
})
|
||||
|
||||
describe("getSkillsAsCommands", () => {
|
||||
it("should return all skills as slash commands automatically", async () => {
|
||||
const skillDir = p(globalSkillsDir, "my-skill")
|
||||
const skillMdPath = p(skillDir, "SKILL.md")
|
||||
|
||||
mockDirectoryExists.mockImplementation(async (p: string) => {
|
||||
if (p === globalSkillsDir) return true
|
||||
return false
|
||||
})
|
||||
mockFileExists.mockImplementation(async (p: string) => p === skillMdPath)
|
||||
mockRealpath.mockImplementation(async (p: string) => p)
|
||||
mockReaddir.mockResolvedValue(["my-skill"])
|
||||
mockStat.mockResolvedValue({ isDirectory: () => true })
|
||||
|
||||
const skillContent = `---
|
||||
name: my-skill
|
||||
description: A test skill
|
||||
---
|
||||
|
||||
# My Skill Instructions`
|
||||
|
||||
mockReadFile.mockResolvedValue(skillContent)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
const commands = skillsManager.getSkillsAsCommands("code")
|
||||
|
||||
expect(commands).toHaveLength(1)
|
||||
expect(commands[0].name).toBe("my-skill")
|
||||
expect(commands[0].skillName).toBe("my-skill")
|
||||
expect(commands[0].description).toBe("A test skill")
|
||||
expect(commands[0].source).toBe("global")
|
||||
})
|
||||
|
||||
it("should filter by mode when getting skills as commands", async () => {
|
||||
const skillDir = p(globalSkillsCodeDir, "code-skill")
|
||||
const skillMdPath = p(skillDir, "SKILL.md")
|
||||
|
||||
mockDirectoryExists.mockImplementation(async (p: string) => {
|
||||
if (p === globalSkillsCodeDir) return true
|
||||
return false
|
||||
})
|
||||
mockFileExists.mockImplementation(async (p: string) => p === skillMdPath)
|
||||
mockRealpath.mockImplementation(async (p: string) => p)
|
||||
mockReaddir.mockResolvedValue(["code-skill"])
|
||||
mockStat.mockResolvedValue({ isDirectory: () => true })
|
||||
|
||||
const skillContent = `---
|
||||
name: code-skill
|
||||
description: A code-specific skill
|
||||
---
|
||||
|
||||
# Code Skill`
|
||||
|
||||
mockReadFile.mockResolvedValue(skillContent)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Should appear for code mode
|
||||
const codeCommands = skillsManager.getSkillsAsCommands("code")
|
||||
expect(codeCommands).toHaveLength(1)
|
||||
|
||||
// Should not appear for architect mode
|
||||
const architectCommands = skillsManager.getSkillsAsCommands("architect")
|
||||
expect(architectCommands).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("dispose", () => {
|
||||
it("should clean up resources", async () => {
|
||||
await skillsManager.dispose()
|
||||
|
|
|
|||
|
|
@ -172,4 +172,16 @@ describe("built-in skills integration", () => {
|
|||
const content = getBuiltInSkillContent("non-existent-skill")
|
||||
expect(content).toBeNull()
|
||||
})
|
||||
|
||||
it("should have accessible skill content for all built-in skills", async () => {
|
||||
const { getBuiltInSkills, getBuiltInSkillContent } = await import("../built-in-skills")
|
||||
|
||||
const skills = getBuiltInSkills()
|
||||
|
||||
// All built-in skills should have accessible content
|
||||
for (const skill of skills) {
|
||||
const content = getBuiltInSkillContent(skill.name)
|
||||
expect(content).not.toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* in the built-in/ directory. To modify built-in skills, edit the corresponding
|
||||
* SKILL.md file and run: pnpm generate:skills
|
||||
*
|
||||
* Generated at: 2026-01-28T23:09:14.137Z
|
||||
* Generated at: 2026-01-30T00:07:07.768Z
|
||||
*/
|
||||
|
||||
import { SkillMetadata, SkillContent } from "../../shared/skills"
|
||||
|
|
@ -32,6 +32,7 @@ Unless the user specifies otherwise, new local MCP servers should be created in
|
|||
MCP servers can be configured in two ways in the MCP settings file:
|
||||
|
||||
1. Local (Stdio) Server Configuration:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
|
|
@ -47,6 +48,7 @@ MCP servers can be configured in two ways in the MCP settings file:
|
|||
\`\`\`
|
||||
|
||||
2. Remote (SSE) Server Configuration:
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
|
|
@ -61,6 +63,7 @@ MCP servers can be configured in two ways in the MCP settings file:
|
|||
\`\`\`
|
||||
|
||||
Common configuration options for both types:
|
||||
|
||||
- \`disabled\`: (optional) Set to true to temporarily disable the server
|
||||
- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60)
|
||||
- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation
|
||||
|
|
@ -105,178 +108,170 @@ weather-server/
|
|||
|
||||
\`\`\`typescript
|
||||
#!/usr/bin/env node
|
||||
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import axios from 'axios';
|
||||
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { z } from "zod"
|
||||
import axios from "axios"
|
||||
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY // provided by MCP config
|
||||
if (!API_KEY) {
|
||||
throw new Error('OPENWEATHER_API_KEY environment variable is required');
|
||||
throw new Error("OPENWEATHER_API_KEY environment variable is required")
|
||||
}
|
||||
|
||||
// Define types for OpenWeather API responses
|
||||
interface WeatherData {
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
weather: Array<{
|
||||
description: string;
|
||||
}>;
|
||||
wind: {
|
||||
speed: number;
|
||||
};
|
||||
main: {
|
||||
temp: number
|
||||
humidity: number
|
||||
}
|
||||
weather: Array<{
|
||||
description: string
|
||||
}>
|
||||
wind: {
|
||||
speed: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ForecastData {
|
||||
list: Array<WeatherData & {
|
||||
dt_txt: string;
|
||||
}>;
|
||||
list: Array<
|
||||
WeatherData & {
|
||||
dt_txt: string
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
// Create an MCP server
|
||||
const server = new McpServer({
|
||||
name: "weather-server",
|
||||
version: "0.1.0"
|
||||
});
|
||||
name: "weather-server",
|
||||
version: "0.1.0",
|
||||
})
|
||||
|
||||
// Create axios instance for OpenWeather API
|
||||
const weatherApi = axios.create({
|
||||
baseURL: 'http://api.openweathermap.org/data/2.5',
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: 'metric',
|
||||
},
|
||||
});
|
||||
baseURL: "http://api.openweathermap.org/data/2.5",
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: "metric",
|
||||
},
|
||||
})
|
||||
|
||||
// Add a tool for getting weather forecasts
|
||||
server.tool(
|
||||
"get_forecast",
|
||||
{
|
||||
city: z.string().describe("City name"),
|
||||
days: z.number().min(1).max(5).optional().describe("Number of days (1-5)"),
|
||||
},
|
||||
async ({ city, days = 3 }) => {
|
||||
try {
|
||||
const response = await weatherApi.get<ForecastData>('forecast', {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: Math.min(days, 5) * 8,
|
||||
},
|
||||
});
|
||||
"get_forecast",
|
||||
{
|
||||
city: z.string().describe("City name"),
|
||||
days: z.number().min(1).max(5).optional().describe("Number of days (1-5)"),
|
||||
},
|
||||
async ({ city, days = 3 }) => {
|
||||
try {
|
||||
const response = await weatherApi.get<ForecastData>("forecast", {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: Math.min(days, 5) * 8,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: \`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: \`Weather API error: \${error.response?.data.message ?? error.message}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Add a resource for current weather in San Francisco
|
||||
server.resource(
|
||||
"sf_weather",
|
||||
{ uri: "weather://San Francisco/current", list: true },
|
||||
async (uri) => {
|
||||
try {
|
||||
const response = weatherApi.get<WeatherData>('weather', {
|
||||
params: { q: "San Francisco" },
|
||||
});
|
||||
server.resource("sf_weather", { uri: "weather://San Francisco/current", list: true }, async (uri) => {
|
||||
try {
|
||||
const response = weatherApi.get<WeatherData>("weather", {
|
||||
params: { q: "San Francisco" },
|
||||
})
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(\`Weather API error: \${error.response?.data.message ?? error.message}\`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
// Add a dynamic resource template for current weather by city
|
||||
server.resource(
|
||||
"current_weather",
|
||||
new ResourceTemplate("weather://{city}/current", { list: true }),
|
||||
async (uri, { city }) => {
|
||||
try {
|
||||
const response = await weatherApi.get('weather', {
|
||||
params: { q: city },
|
||||
});
|
||||
"current_weather",
|
||||
new ResourceTemplate("weather://{city}/current", { list: true }),
|
||||
async (uri, { city }) => {
|
||||
try {
|
||||
const response = await weatherApi.get("weather", {
|
||||
params: { q: city },
|
||||
})
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(\`Weather API error: \${error.response?.data.message ?? error.message}\`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Start receiving messages on stdin and sending messages on stdout
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('Weather MCP server running on stdio');
|
||||
const transport = new StdioServerTransport()
|
||||
await server.connect(transport)
|
||||
console.error("Weather MCP server running on stdio")
|
||||
\`\`\`
|
||||
|
||||
(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue