diff --git a/executable/persisted/tests/Test.js b/executable/persisted/tests/Test.js index d86da41..a31259a 100644 --- a/executable/persisted/tests/Test.js +++ b/executable/persisted/tests/Test.js @@ -9,9 +9,6 @@ const { testDescription = getTestDescription("snippets", __dirname); -const requestsFile = path.join(path.dirname(__dirname), "operations.graphql"); -const requests = fs.readFileSync(requestsFile, "utf8").toString(); - describe(testDescription, function () { const tests = [ { diff --git a/executable/prescribed/README.md b/executable/prescribed/README.md new file mode 100644 index 0000000..040cfe1 --- /dev/null +++ b/executable/prescribed/README.md @@ -0,0 +1,141 @@ +# Prescribed Tools + +A **prescribed tool** is a tool that maps to a specific [GraphQL operation](https://spec.graphql.org/September2025/#sec-Language.Operations) in a [persisted document](https://spec.graphql.org/September2025/#sec-Persisted-Documents). The `@tool(prescribed:)` argument links the tool definition to an operation name in a persisted document. This approach provides a structured way to expose specific GraphQL operations as tools that can be called by AI models. + +This sample demonstrates how to use the `@tool` directive to create prescribed tools for MCP (Model Context Protocol). + +## Overview + +The sample implements a mock weather service API with a single operation: weather forecast for a city. + +The key feature demonstrated is how to create a prescribed tool that maps to a persisted GraphQL operation, allowing AI models to execute specific, well-defined queries. + +## Schema Structure + +The schema consists of: + +1. A main schema file (`index.graphql`) that defines: + - The GraphQL types for weather data + - The `@tool` directive that creates a prescribed tool + - The `@sdl` directive that includes the persisted operation + +2. An operations file (`operations.graphql`) that contains: + - A GraphQL operation that will be exposed as a tool + - Descriptions for the operation and its variables (recommended best practice) + +## How Prescribed Tools Work + +A prescribed tool is defined using the `@tool` directive with the `prescribed` argument pointing to a specific operation in a persisted document: + +```graphql +schema + # Load the persisted operations document that contains the WeatherForecast operation + @sdl( + files: [] + executables: [{ document: "operations.graphql", persist: true }] + ) + # Define a prescribed tool that maps to the WeatherForecast operation in the persisted document + @tool(name: "weather-lookup", prescribed: "WeatherForecast") { + query: Query +} +``` + +The operation in the persisted document should include descriptions ([new to GraphQL September 2025](https://spec.graphql.org/September2025/#Description)) to help AI models understand how to use the tool: + +```graphql +""" +Get detailed weather forecast for a specific city +This operation provides a multi-day weather forecast including temperature, conditions, and other meteorological data +""" +query WeatherForecast( + """The name of the city to get weather forecast for""" + $city: String!, + """Number of days to forecast (1-7), defaults to 3 days""" + $days: Int = 3 +) { + weatherForecast(city: $city, days: $days) { + city { + name + country + timezone + } + forecast { + date + conditions + high { + celsius + fahrenheit + } + low { + celsius + fahrenheit + } + precipitation + humidity + windSpeed + windDirection + } + } +} +``` + +## MCP Tool Description + +When deployed, this schema will expose a tool through the MCP endpoint. The tool's description and parameter information are derived from the GraphQL operation's descriptions: + +```json +{ + "name": "weather-lookup", + "description": "Get detailed weather forecast for a specific city. This operation provides a multi-day weather forecast including temperature, conditions, and other meteorological data", + "inputSchema": { + "type": "object", + "properties": { + "variables": { + "properties": { + "city": { + "description": "The name of the city to get weather forecast for", + "type": "string" + }, + "days": { + "description": "Number of days to forecast (1-7), defaults to 3 days", + "type": "integer", + "default": 3 + } + }, + "required": ["city"], + "type": "object" + } + }, + "required": ["variables"] + } +} +``` + +## Using the Tool + +An AI model can use this tool by: + +1. Understanding the tool's purpose from the operation's description +2. Providing the required variables (city name and optionally number of days) +3. Receiving structured weather forecast data in response + +## Benefits of Prescribed Tools + +1. **Controlled Access**: Only specific, predefined operations are exposed as tools, providing fine-grained control over what AI models can execute. +2. **Type Safety**: The GraphQL schema ensures that inputs are properly validated. +3. **Clear Documentation**: Operation descriptions serve as both documentation for developers and instructions for AI models. +4. **Versioning**: Operations can be versioned and updated independently of the tool definition. + +## Testing as an MCP Tool + +To test this as an MCP tool with AI models: + +1. Deploy the schema to StepZen using the command `stepzen deploy` +2. [Connect Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers) to your StepZen MCP endpoint +3. The tool will appear as `weather-lookup` and can be called by the AI model + +**Example**: Interaction between MCP and the Claude UI. + +Image + +Image \ No newline at end of file diff --git a/executable/prescribed/index.graphql b/executable/prescribed/index.graphql new file mode 100644 index 0000000..726b8f1 --- /dev/null +++ b/executable/prescribed/index.graphql @@ -0,0 +1,185 @@ +schema + # Load the persisted operations document that contains the WeatherForecast operation + @sdl( + files: [] + executables: [{ document: "operations.graphql", persist: true }] + ) + # Define a prescribed tool that maps to the WeatherForecast operation in the persisted document + @tool(name: "weather-lookup", prescribed: "WeatherForecast") { + query: Query +} + +type Query { + # Get weather forecast for a specific city + weatherForecast(city: String!, days: Int = 1): WeatherForecast + @value( + script: { + src: """ + function weatherForecast() { + const cities = { + "new york": { + name: "New York", + country: "United States", + latitude: 40.7128, + longitude: -74.0060, + timezone: "America/New_York", + population: 8804190 + }, + "london": { + name: "London", + country: "United Kingdom", + latitude: 51.5074, + longitude: -0.1278, + timezone: "Europe/London", + population: 8982000 + }, + "tokyo": { + name: "Tokyo", + country: "Japan", + latitude: 35.6762, + longitude: 139.6503, + timezone: "Asia/Tokyo", + population: 13960000 + }, + "sydney": { + name: "Sydney", + country: "Australia", + latitude: -33.8688, + longitude: 151.2093, + timezone: "Australia/Sydney", + population: 5312000 + } + }; + + // Default to 1 day if not specified or out of range + const daysToForecast = days < 1 || days > 7 ? 1 : days; + + const normalizedCity = city.toLowerCase(); + const cityData = cities[normalizedCity] || { + name: city, + country: "Unknown", + latitude: 0, + longitude: 0, + timezone: "UTC", + population: null + }; + + // Fixed forecast patterns for each city + const forecastPatterns = { + "new york": [ + { conditions: "Partly Cloudy", high: 22, low: 15, precipitation: 20, humidity: 65, windSpeed: 12, windDirection: "NW" }, + { conditions: "Sunny", high: 24, low: 16, precipitation: 5, humidity: 55, windSpeed: 8, windDirection: "W" }, + { conditions: "Cloudy", high: 20, low: 14, precipitation: 30, humidity: 70, windSpeed: 15, windDirection: "NE" }, + { conditions: "Rain", high: 18, low: 12, precipitation: 75, humidity: 85, windSpeed: 18, windDirection: "E" }, + { conditions: "Partly Cloudy", high: 23, low: 16, precipitation: 15, humidity: 60, windSpeed: 10, windDirection: "SW" }, + { conditions: "Sunny", high: 25, low: 17, precipitation: 0, humidity: 50, windSpeed: 7, windDirection: "W" }, + { conditions: "Thunderstorm", high: 21, low: 15, precipitation: 90, humidity: 90, windSpeed: 25, windDirection: "S" } + ], + "london": [ + { conditions: "Cloudy", high: 16, low: 10, precipitation: 40, humidity: 75, windSpeed: 14, windDirection: "SW" }, + { conditions: "Rain", high: 15, low: 9, precipitation: 65, humidity: 80, windSpeed: 16, windDirection: "W" }, + { conditions: "Partly Cloudy", high: 17, low: 11, precipitation: 25, humidity: 70, windSpeed: 12, windDirection: "NW" }, + { conditions: "Cloudy", high: 16, low: 10, precipitation: 35, humidity: 75, windSpeed: 13, windDirection: "SW" }, + { conditions: "Rain", high: 14, low: 8, precipitation: 70, humidity: 85, windSpeed: 18, windDirection: "W" }, + { conditions: "Partly Cloudy", high: 18, low: 12, precipitation: 20, humidity: 65, windSpeed: 11, windDirection: "NW" }, + { conditions: "Sunny", high: 19, low: 13, precipitation: 10, humidity: 60, windSpeed: 9, windDirection: "N" } + ], + "tokyo": [ + { conditions: "Sunny", high: 26, low: 19, precipitation: 5, humidity: 60, windSpeed: 10, windDirection: "E" }, + { conditions: "Partly Cloudy", high: 25, low: 18, precipitation: 15, humidity: 65, windSpeed: 12, windDirection: "SE" }, + { conditions: "Cloudy", high: 23, low: 17, precipitation: 30, humidity: 70, windSpeed: 14, windDirection: "S" }, + { conditions: "Rain", high: 22, low: 16, precipitation: 60, humidity: 80, windSpeed: 16, windDirection: "SW" }, + { conditions: "Partly Cloudy", high: 24, low: 18, precipitation: 20, humidity: 65, windSpeed: 11, windDirection: "E" }, + { conditions: "Sunny", high: 27, low: 20, precipitation: 5, humidity: 55, windSpeed: 9, windDirection: "NE" }, + { conditions: "Cloudy", high: 24, low: 18, precipitation: 25, humidity: 68, windSpeed: 13, windDirection: "SE" } + ], + "sydney": [ + { conditions: "Sunny", high: 28, low: 20, precipitation: 10, humidity: 60, windSpeed: 15, windDirection: "NE" }, + { conditions: "Partly Cloudy", high: 27, low: 19, precipitation: 15, humidity: 65, windSpeed: 14, windDirection: "E" }, + { conditions: "Sunny", high: 29, low: 21, precipitation: 5, humidity: 55, windSpeed: 13, windDirection: "NE" }, + { conditions: "Partly Cloudy", high: 26, low: 19, precipitation: 20, humidity: 70, windSpeed: 16, windDirection: "SE" }, + { conditions: "Cloudy", high: 24, low: 18, precipitation: 35, humidity: 75, windSpeed: 18, windDirection: "S" }, + { conditions: "Rain", high: 22, low: 17, precipitation: 70, humidity: 85, windSpeed: 20, windDirection: "SW" }, + { conditions: "Partly Cloudy", high: 25, low: 18, precipitation: 25, humidity: 70, windDirection: "W", windSpeed: 17 } + ] + }; + + // Get pattern for city or use default + const pattern = forecastPatterns[normalizedCity] || forecastPatterns["new york"]; + + // Generate forecast for the requested number of days + const forecast = []; + const today = new Date(); + + for (let i = 0; i < daysToForecast; i++) { + const forecastDate = new Date(); + forecastDate.setDate(today.getDate() + i); + + const dayPattern = pattern[i % pattern.length]; + + forecast.push({ + date: forecastDate.toISOString().split('T')[0], + sunrise: "06:25 AM", + sunset: "06:35 PM", + high: { + celsius: dayPattern.high, + fahrenheit: Math.round(dayPattern.high * 9 / 5 + 32) + }, + low: { + celsius: dayPattern.low, + fahrenheit: Math.round(dayPattern.low * 9 / 5 + 32) + }, + conditions: dayPattern.conditions, + precipitation: dayPattern.precipitation, + humidity: dayPattern.humidity, + windSpeed: dayPattern.windSpeed, + windDirection: dayPattern.windDirection + }); + } + + return { + city: cityData, + forecast: forecast, + }; + } + weatherForecast() + """ + } + ) +} + +type WeatherForecast { + city: City! + forecast: [DailyForecast!]! + lastUpdated: DateTime +} + +type City { + name: String! + country: String! + latitude: Float! + longitude: Float! + timezone: String! + population: Int +} + +type DailyForecast { + date: Date! + sunrise: String! + sunset: String! + high: Temperature! + low: Temperature! + conditions: String! + precipitation: Float! + humidity: Int! + windSpeed: Float! + windDirection: String! +} + +type Temperature { + celsius: Float! + fahrenheit: Float! +} + +scalar Date +scalar DateTime diff --git a/executable/prescribed/operations.graphql b/executable/prescribed/operations.graphql new file mode 100644 index 0000000..a220226 --- /dev/null +++ b/executable/prescribed/operations.graphql @@ -0,0 +1,33 @@ +""" +Get detailed weather forecast for a specific city +This operation provides a multi-day weather forecast including temperature, conditions, and other meteorological data +""" +query WeatherForecast( + """The name of the city to get weather forecast for""" + $city: String!, + """Number of days to forecast (1-7), defaults to current day""" + $days: Int = 1 +) { + weatherForecast(city: $city, days: $days) { + city { + name + country + timezone + } + forecast { + high { + celsius + fahrenheit + } + low { + celsius + fahrenheit + } + conditions + precipitation + humidity + windSpeed + windDirection + } + } +} diff --git a/executable/prescribed/stepzen.config.json b/executable/prescribed/stepzen.config.json new file mode 100644 index 0000000..af1c0ea --- /dev/null +++ b/executable/prescribed/stepzen.config.json @@ -0,0 +1,3 @@ +{ + "endpoint": "api/miscellaneous" +} diff --git a/executable/prescribed/tests/Test.js b/executable/prescribed/tests/Test.js new file mode 100644 index 0000000..2b8819a --- /dev/null +++ b/executable/prescribed/tests/Test.js @@ -0,0 +1,48 @@ +const { + deployAndRun, + stepzen, + getTestDescription, +} = require("../../../tests/gqltest.js"); + +testDescription = getTestDescription("snippets", __dirname); + +describe(testDescription, function () { + const tests = [ + { + label: "WeatherForecast", + documentId: + "sha256:eb01da571734ea3074e4aa494398f947ff76c3b339ad0c15d5c9127b5f53ac4d", + operationName: "WeatherForecast", + variables: { + city: "Sydney", + }, + expected: { + weatherForecast: { + city: { + name: "Sydney", + country: "Australia", + timezone: "Australia/Sydney" + }, + forecast: [ + { + high: { + celsius: 28, + fahrenheit: 82 + }, + low: { + celsius: 20, + fahrenheit: 68 + }, + conditions: "Sunny", + precipitation: 10, + humidity: 60, + windSpeed: 15, + windDirection: "NE" + }, + ], + }, + }, + }, + ]; + return deployAndRun(__dirname, tests, stepzen.admin); +});