Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions executable/persisted/tests/Test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down
141 changes: 141 additions & 0 deletions executable/prescribed/README.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

persisted document is not a GraphQL spec concept, at least not that spec, maybe a reference to the persisted snippet.


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.

<img width="563" height="360" alt="Image" src="https://github.com/user-attachments/assets/7da0cd20-2469-45cd-8a17-0891a1a03dec" />

<img width="502" height="588" alt="Image" src="https://github.com/user-attachments/assets/cf499b7e-a3fe-433c-b837-bdde25441739" />
185 changes: 185 additions & 0 deletions executable/prescribed/index.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
schema
# Load the persisted operations document that contains the WeatherForecast operation
@sdl(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a useful comment

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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a useful comment

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
33 changes: 33 additions & 0 deletions executable/prescribed/operations.graphql
Original file line number Diff line number Diff line change
@@ -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
}
}
}
3 changes: 3 additions & 0 deletions executable/prescribed/stepzen.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"endpoint": "api/miscellaneous"
}
Loading