├── .github ├── CODEOWNERS └── workflows │ └── pr-build.yml ├── .gitignore ├── LICENSE ├── README.md ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── scripts └── generate-version.js ├── src ├── config │ ├── env.ts │ ├── sanity.ts │ └── version.ts ├── index.ts ├── prompts │ └── register.ts ├── resources │ └── register.ts ├── tools │ ├── context │ │ ├── getInitialContextTool.ts │ │ ├── instructions.ts │ │ ├── middleware.ts │ │ ├── register.ts │ │ └── store.ts │ ├── datasets │ │ ├── createDatasetTool.ts │ │ ├── deleteDatasetTool.ts │ │ ├── listDatasets.ts │ │ ├── register.ts │ │ └── updateDatasetTool.ts │ ├── documents │ │ ├── createDocumentTool.ts │ │ ├── createVersionTool.ts │ │ ├── documentActionsTool.ts │ │ ├── patchDocumentTool.ts │ │ ├── queryDocumentsTool.ts │ │ ├── register.ts │ │ ├── transformDocumentTool.ts │ │ ├── transformImageTool.ts │ │ ├── translateDocumentTool.ts │ │ └── updateDocumentTool.ts │ ├── embeddings │ │ ├── listEmbeddingsTool.ts │ │ ├── register.ts │ │ └── semanticSearchTool.ts │ ├── groq │ │ ├── getGroqSpecification.ts │ │ └── register.ts │ ├── projects │ │ ├── getProjectStudiosTool.ts │ │ ├── listProjectsTool.ts │ │ └── register.ts │ ├── register.ts │ ├── releases │ │ ├── common.ts │ │ ├── createReleaseTool.ts │ │ ├── editReleaseTool.ts │ │ ├── listReleases.ts │ │ ├── register.ts │ │ ├── releaseActionsTool.ts │ │ └── scheduleReleaseTool.ts │ └── schema │ │ ├── common.ts │ │ ├── getSchemaTool.ts │ │ ├── listWorkspaceSchemasTool.ts │ │ └── register.ts ├── types │ ├── any.ts │ ├── manifest.ts │ ├── mcp.ts │ └── sanity.ts └── utils │ ├── dates.ts │ ├── formatters.ts │ ├── groq.ts │ ├── id.ts │ ├── path.ts │ ├── resolvers.ts │ ├── response.ts │ ├── schema.ts │ ├── tokens.ts │ └── tools.ts └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @RostiMelk -------------------------------------------------------------------------------- /.github/workflows/pr-build.yml: -------------------------------------------------------------------------------- 1 | name: PR Build Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: "setup pnpm" 15 | uses: pnpm/action-setup@v4 16 | with: 17 | version: 10 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: "22" 23 | cache: "pnpm" 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Run build 29 | run: pnpm run build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | build/ 9 | dist/ 10 | out/ 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.*.local 16 | 17 | # IDE and editor files 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 - 2024 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanity MCP Server 2 | 3 | > Transform your content operations with AI-powered tools for Sanity. Create, manage, and explore your content through natural language conversations in your favorite AI-enabled editor. 4 | 5 | Sanity MCP Server implements the [Model Context Protocol](https://modelcontextprotocol.ai) to connect your Sanity projects with AI tools like Claude, Cursor, and VS Code. It enables AI models to understand your content structure and perform operations through natural language instructions. 6 | 7 | ## ✨ Key Features 8 | 9 | - 🤖 **Content Intelligence**: Let AI explore and understand your content library 10 | - 🔄 **Content Operations**: Automate tasks through natural language instructions 11 | - 📊 **Schema-Aware**: AI respects your content structure and validation rules 12 | - 🚀 **Release Management**: Plan and organize content releases effortlessly 13 | - 🔍 **Semantic Search**: Find content based on meaning, not just keywords 14 | 15 | ## Table of Contents 16 | 17 | - [🔌 Quickstart](#-quickstart) 18 | - [Prerequisites](#prerequisites) 19 | - [Add configuration for the Sanity MCP server](#add-configuration-for-the-sanity-mcp-server) 20 | - [🛠️ Available Tools](#️-available-tools) 21 | - [⚙️ Configuration](#️-configuration) 22 | - [🔑 API Tokens and Permissions](#-api-tokens-and-permissions) 23 | - [👥 User Roles](#-user-roles) 24 | - [📦 Node.js Environment Setup](#-nodejs-environment-setup) 25 | - [🛠 Quick Setup for Node Version Manager Users](#-quick-setup-for-node-version-manager-users) 26 | - [🤔 Why Is This Needed?](#-why-is-this-needed) 27 | - [🔍 Troubleshooting](#-troubleshooting) 28 | - [💻 Development](#-development) 29 | - [Debugging](#debugging) 30 | 31 | ## 🔌 Quickstart 32 | 33 | ### Prerequisites 34 | 35 | Before you can use the MCP server, you need to: 36 | 37 | 1. **Deploy your Sanity Studio with schema manifest** 38 | 39 | The MCP server needs access to your content structure to work effectively. Deploy your schema manifest using one of these approaches: 40 | 41 | ```bash 42 | # Option A: If you have the CLI installed globally 43 | npm install -g sanity 44 | cd /path/to/studio 45 | sanity schema deploy 46 | 47 | # Option B: Update your Studio 48 | cd /path/to/studio 49 | npm update sanity 50 | npx sanity schema deploy 51 | ``` 52 | 53 | When running in CI environments without Sanity login, you'll need to provide an auth token: 54 | 55 | ```bash 56 | SANITY_AUTH_TOKEN= sanity schema deploy 57 | ``` 58 | 59 | > [!NOTE] 60 | > Schema deployment requires Sanity CLI version 3.88.1 or newer. 61 | 62 | 2. **Get your API credentials** 63 | - Project ID 64 | - Dataset name 65 | - API token with appropriate permissions 66 | 67 | This MCP server can be used with any application that supports the Model Context Protocol. Here are some popular examples: 68 | 69 | - [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) 70 | - [Cursor IDE](https://docs.cursor.com/context/model-context-protocol) 71 | - [Visual Studio Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) 72 | - Custom MCP-compatible applications 73 | 74 | ### Add configuration for the Sanity MCP server 75 | 76 | To use the Sanity MCP server, add the following configuration to your application's MCP settings: 77 | 78 | ```json 79 | { 80 | "mcpServers": { 81 | "sanity": { 82 | "command": "npx", 83 | "args": ["-y", "@sanity/mcp-server@latest"], 84 | "env": { 85 | "SANITY_PROJECT_ID": "your-project-id", 86 | "SANITY_DATASET": "production", 87 | "SANITY_API_TOKEN": "your-sanity-api-token", 88 | "MCP_USER_ROLE": "developer" 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | For a complete list of all required and optional environment variables, see the [Configuration section](#️-configuration). 96 | 97 | The exact location of this configuration will depend on your application: 98 | 99 | | Application | Configuration Location | 100 | | -------------- | ------------------------------------------------- | 101 | | Claude Desktop | Claude Desktop configuration file | 102 | | Cursor | Workspace or global settings | 103 | | VS Code | Workspace or user settings (depends on extension) | 104 | | Custom Apps | Refer to your app's MCP integration docs | 105 | 106 | You don't get it to work? See the section on [Node.js configuration](#-nodejs-environment-setup). 107 | 108 | ## 🛠️ Available Tools 109 | 110 | ### Context & Setup 111 | 112 | - **get_initial_context** – IMPORTANT: Must be called before using any other tools to initialize context and get usage instructions. 113 | - **get_sanity_config** – Retrieves current Sanity configuration (projectId, dataset, apiVersion, etc.) 114 | 115 | ### Document Operations 116 | 117 | - **create_document** – Create a new document with AI-generated content based on instructions 118 | - **update_document** – Update an existing document with AI-generated content based on instructions 119 | - **patch_document** - Apply direct patch operations to modify specific parts of a document without using AI generation 120 | - **transform_document** – Transform document content while preserving formatting and structure, ideal for text replacements and style corrections 121 | - **translate_document** – Translate document content to another language while preserving formatting and structure 122 | - **query_documents** – Execute GROQ queries to search for and retrieve content 123 | - **document_action** – Perform document actions like publishing, unpublishing, or deleting documents 124 | 125 | ### Release Management 126 | 127 | - **list_releases** – List content releases, optionally filtered by state 128 | - **create_release** – Create a new content release 129 | - **edit_release** – Update metadata for an existing release 130 | - **schedule_release** – Schedule a release to publish at a specific time 131 | - **release_action** – Perform actions on releases (publish, archive, unarchive, unschedule, delete) 132 | 133 | ### Version Management 134 | 135 | - **create_version** – Create a version of a document for a specific release 136 | - **discard_version** – Delete a specific version document from a release 137 | - **mark_for_unpublish** – Mark a document to be unpublished when a specific release is published 138 | 139 | ### Dataset Management 140 | 141 | - **list_datasets** – List all datasets in the project 142 | - **create_dataset** – Create a new dataset 143 | - **update_dataset** – Modify dataset settings 144 | 145 | ### Schema Information 146 | 147 | - **get_schema** – Get schema details, either full schema or for a specific type 148 | - **list_workspace_schemas** – Get a list of all available workspace schema names 149 | 150 | ### GROQ Support 151 | 152 | - **get_groq_specification** – Get the GROQ language specification summary 153 | 154 | ### Embeddings & Semantic Search 155 | 156 | - **list_embeddings_indices** – List all available embeddings indices 157 | - **semantic_search** – Perform semantic search on an embeddings index 158 | 159 | ### Project Information 160 | 161 | - **list_projects** – List all Sanity projects associated with your account 162 | - **get_project_studios** – Get studio applications linked to a specific project 163 | 164 | ## ⚙️ Configuration 165 | 166 | The server takes the following environment variables: 167 | 168 | | Variable | Description | Required | 169 | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | 170 | | `SANITY_API_TOKEN` | Your Sanity API token | ✅ | 171 | | `SANITY_PROJECT_ID` | Your Sanity project ID | ✅ | 172 | | `SANITY_DATASET` | The dataset to use | ✅ | 173 | | `MCP_USER_ROLE` | Determines tool access level (developer or editor) | ✅ | 174 | | `SANITY_API_HOST` | API host (defaults to https://api.sanity.io) | ❌ | 175 | | `MAX_TOOL_TOKEN_OUTPUT` | Maximum token output for tool responses (defaults to 50000). Adjust based on your model's context limits. Higher limits may pollute the conversation context with excessive data | ❌ | 176 | 177 | > [!WARNING] > **Using AI with Production Datasets** 178 | > When configuring the MCP server with a token that has write access to a production dataset, please be aware that the AI can perform destructive actions like creating, updating, or deleting content. This is not a concern if you're using a read-only token. While we are actively developing guardrails, you should exercise caution and consider using a development/staging dataset for testing AI operations that require write access. 179 | 180 | ### 🔑 API Tokens and Permissions 181 | 182 | The MCP server requires appropriate API tokens and permissions to function correctly. Here's what you need to know: 183 | 184 | 1. **Generate a Robot Token**: 185 | 186 | - Go to your project's management console: Settings > API > Tokens 187 | - Click "Add new token" 188 | - Create a dedicated token for your MCP server usage 189 | - Store the token securely - it's only shown once! 190 | 191 | 2. **Required Permissions**: 192 | 193 | - The token needs appropriate permissions based on your usage 194 | - For basic read operations: `viewer` role is sufficient 195 | - For content management: `editor` or `developer` role recommended 196 | - For advanced operations (like managing datasets): `administrator` role may be needed 197 | 198 | 3. **Dataset Access**: 199 | 200 | - Public datasets: Content is readable by unauthenticated users 201 | - Private datasets: Require proper token authentication 202 | - Draft and versioned content: Only accessible to authenticated users with appropriate permissions 203 | 204 | 4. **Security Best Practices**: 205 | - Use separate tokens for different environments (development, staging, production) 206 | - Never commit tokens to version control 207 | - Consider using environment variables for token management 208 | - Regularly rotate tokens for security 209 | 210 | ### 👥 User Roles 211 | 212 | The server supports two user roles: 213 | 214 | - **developer**: Access to all tools 215 | - **editor**: Content-focused tools without project administration 216 | 217 | ## 📦 Node.js Environment Setup 218 | 219 | > **Important for Node Version Manager Users**: If you use `nvm`, `mise`, `fnm`, `nvm-windows` or similar tools, you'll need to follow the setup steps below to ensure MCP servers can access Node.js. This is a one-time setup that will save you troubleshooting time later. This is [an ongoing issue](https://github.com/modelcontextprotocol/servers/issues/64) with MCP servers. 220 | 221 | ### 🛠 Quick Setup for Node Version Manager Users 222 | 223 | 1. First, activate your preferred Node.js version: 224 | 225 | ```bash 226 | # Using nvm 227 | nvm use 20 # or your preferred version 228 | 229 | # Using mise 230 | mise use node@20 231 | 232 | # Using fnm 233 | fnm use 20 234 | ``` 235 | 236 | 2. Then, create the necessary symlinks (choose your OS): 237 | 238 | **On macOS/Linux:** 239 | 240 | ```bash 241 | sudo ln -sf "$(which node)" /usr/local/bin/node && sudo ln -sf "$(which npx)" /usr/local/bin/npx 242 | ``` 243 | 244 | > [!NOTE] 245 | > While using `sudo` generally requires caution, it's safe in this context because: 246 | > 247 | > - We're only creating symlinks to your existing Node.js binaries 248 | > - The target directory (`/usr/local/bin`) is a standard system location for user-installed programs 249 | > - The symlinks only point to binaries you've already installed and trust 250 | > - You can easily remove these symlinks later with `sudo rm` 251 | 252 | **On Windows (PowerShell as Administrator):** 253 | 254 | ```powershell 255 | New-Item -ItemType SymbolicLink -Path "C:\Program Files\nodejs\node.exe" -Target (Get-Command node).Source -Force 256 | New-Item -ItemType SymbolicLink -Path "C:\Program Files\nodejs\npx.cmd" -Target (Get-Command npx).Source -Force 257 | ``` 258 | 259 | 3. Verify the setup: 260 | ```bash 261 | # Should show your chosen Node version 262 | /usr/local/bin/node --version # macOS/Linux 263 | "C:\Program Files\nodejs\node.exe" --version # Windows 264 | ``` 265 | 266 | ### 🤔 Why Is This Needed? 267 | 268 | MCP servers are launched by calling `node` and `npx` binaries directly. When using Node version managers, these binaries are managed in isolated environments that aren't automatically accessible to system applications. The symlinks above create a bridge between your version manager and the system paths that MCP servers use. 269 | 270 | ### 🔍 Troubleshooting 271 | 272 | If you switch Node versions often: 273 | 274 | - Remember to update your symlinks when changing Node versions 275 | - You can create a shell alias or script to automate this: 276 | ```bash 277 | # Example alias for your .bashrc or .zshrc 278 | alias update-node-symlinks='sudo ln -sf "$(which node)" /usr/local/bin/node && sudo ln -sf "$(which npx)" /usr/local/bin/npx' 279 | ``` 280 | 281 | To remove the symlinks later: 282 | 283 | ```bash 284 | # macOS/Linux 285 | sudo rm /usr/local/bin/node /usr/local/bin/npx 286 | 287 | # Windows (PowerShell as Admin) 288 | Remove-Item "C:\Program Files\nodejs\node.exe", "C:\Program Files\nodejs\npx.cmd" 289 | ``` 290 | 291 | ## 💻 Development 292 | 293 | Install dependencies: 294 | 295 | ```bash 296 | pnpm install 297 | ``` 298 | 299 | Build and run in development mode: 300 | 301 | ```bash 302 | pnpm run dev 303 | ``` 304 | 305 | Build the server: 306 | 307 | ```bash 308 | pnpm run build 309 | ``` 310 | 311 | Run the built server: 312 | 313 | ```bash 314 | pnpm start 315 | ``` 316 | 317 | ### Debugging 318 | 319 | For debugging, you can use the MCP inspector: 320 | 321 | ```bash 322 | npx @modelcontextprotocol/inspector -e SANITY_API_TOKEN= -e SANITY_PROJECT_ID= -e SANITY_DATASET= -e MCP_USER_ROLE=developer node path/to/build/index.js 323 | ``` 324 | 325 | This will provide a web interface for inspecting and testing the available tools. 326 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin' 2 | import typescriptParser from '@typescript-eslint/parser' 3 | import simpleImportSort from 'eslint-plugin-simple-import-sort' 4 | import unusedImports from 'eslint-plugin-unused-imports' 5 | 6 | export default [ 7 | { 8 | files: ['**/*.ts', '**/*.tsx'], 9 | languageOptions: { 10 | parser: typescriptParser, 11 | }, 12 | plugins: { 13 | '@typescript-eslint': typescriptEslint, 14 | 'simple-import-sort': simpleImportSort, 15 | 'unused-imports': unusedImports, 16 | }, 17 | rules: { 18 | ...typescriptEslint.configs.recommended.rules, 19 | '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}], 20 | }, 21 | }, 22 | { 23 | // Ignore patterns (equivalent to ignores in the original config) 24 | ignores: ['/build/**'], 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sanity/mcp-server", 3 | "version": "0.10.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/sanity-io/sanity-mcp-server" 7 | }, 8 | "license": "MIT", 9 | "type": "module", 10 | "bin": { 11 | "sanity-mcp-server": "./build/index.js" 12 | }, 13 | "files": [ 14 | "build" 15 | ], 16 | "scripts": { 17 | "prebuild": "node scripts/generate-version.js", 18 | "build": "tsc && chmod 755 build/index.js", 19 | "dev": "nodemon --ext ts --ignore build/ --watch src/ --exec 'npm run build && npm start'", 20 | "format": "prettier --write .", 21 | "lint": "eslint .", 22 | "lint:fix": "eslint . --fix", 23 | "prepublishOnly": "npm run build", 24 | "start": "node build/index.js", 25 | "test": "vitest", 26 | "test:run": "vitest run" 27 | }, 28 | "prettier": "@sanity/prettier-config", 29 | "dependencies": { 30 | "@modelcontextprotocol/sdk": "^1.12.0", 31 | "@sanity/client": "7.4.0", 32 | "@sanity/id-utils": "^1.0.0", 33 | "@sanity/types": "^3.89.0", 34 | "chrono-node": "^2.8.2", 35 | "dotenv": "^16.4.7", 36 | "fast-xml-parser": "^5.2.0", 37 | "get-it": "^8.3.2", 38 | "gpt-tokenizer": "^2.9.0", 39 | "groq-js": "^1.17.0", 40 | "outdent": "^0.8.0", 41 | "zod": "^3.24.2" 42 | }, 43 | "devDependencies": { 44 | "@sanity/prettier-config": "^1.0.3", 45 | "@types/minimist": "^1.2.5", 46 | "@types/node": "^22.13.11", 47 | "@typescript-eslint/eslint-plugin": "^8.29.1", 48 | "eslint-plugin-simple-import-sort": "^12.1.1", 49 | "eslint-plugin-unused-imports": "^4.1.4", 50 | "nodemon": "^3.1.0", 51 | "prettier": "^3.5.3", 52 | "typescript": "^5.8.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/generate-version.js: -------------------------------------------------------------------------------- 1 | import {readFileSync, writeFileSync} from 'node:fs' 2 | import {join} from 'node:path' 3 | import {fileURLToPath} from 'node:url' 4 | import {dirname} from 'node:path' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = dirname(__filename) 8 | 9 | // Read package.json using import instead of require 10 | const packageJsonPath = join(__dirname, '../package.json') 11 | const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) 12 | 13 | const outputPath = join(__dirname, '../src/config/version.ts') 14 | 15 | // Generate version file content directly 16 | const content = `// Generated file - do not edit 17 | export const VERSION = '${packageJson.version}'; 18 | ` 19 | 20 | writeFileSync(outputPath, content) 21 | console.info(`Generated version.ts with version ${packageJson.version}`) 22 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import {z} from 'zod' 3 | dotenv.config() 4 | 5 | const CommonEnvSchema = z.object({ 6 | SANITY_API_TOKEN: z.string().describe('Sanity API token'), 7 | SANITY_API_HOST: z 8 | .string() 9 | .optional() 10 | .default('https://api.sanity.io') 11 | .describe('Sanity API host'), 12 | 13 | INTERNAL_REQUESTER_HEADERS: z 14 | .string() 15 | .optional() 16 | .transform((v) => (v ? JSON.parse(v) : undefined)), 17 | INTERNAL_REQUEST_TAG_PREFIX: z.string().optional(), 18 | INTERNAL_USE_PROJECT_HOSTNAME: z 19 | .union([z.literal('true').transform(() => true), z.literal('false').transform(() => false)]) 20 | .optional(), 21 | 22 | MAX_TOOL_TOKEN_OUTPUT: z.coerce 23 | .number() 24 | .optional() 25 | .default(50000) 26 | .describe('Maximum tool token output'), 27 | }) 28 | 29 | const DefaultSchema = z 30 | .object({ 31 | MCP_USER_ROLE: z.enum(['developer', 'editor']), 32 | SANITY_PROJECT_ID: z.string().describe('Sanity project ID'), 33 | SANITY_DATASET: z.string().describe('The dataset'), 34 | }) 35 | .merge(CommonEnvSchema) 36 | 37 | const AgentSchema = z 38 | .object({ 39 | MCP_USER_ROLE: z.literal('internal_agent_role'), 40 | }) 41 | .merge(CommonEnvSchema) 42 | 43 | const EnvSchema = z.discriminatedUnion('MCP_USER_ROLE', [DefaultSchema, AgentSchema]) 44 | 45 | export const env = EnvSchema.safeParse(process.env) 46 | 47 | if (!env.success) { 48 | console.error('Invalid environment variables', env.error.format()) 49 | process.exit(1) 50 | } 51 | -------------------------------------------------------------------------------- /src/config/sanity.ts: -------------------------------------------------------------------------------- 1 | import {requester as baseRequester, type ClientConfig} from '@sanity/client' 2 | import {headers} from 'get-it/middleware' 3 | import {env} from '../config/env.js' 4 | 5 | /** 6 | * Creates a default Sanity client configuration without actually initializing it. 7 | */ 8 | export function getDefaultClientConfig(): ClientConfig { 9 | if (!env.success) { 10 | throw new Error('Environment variables are not properly configured') 11 | } 12 | 13 | const clientConfig: ClientConfig = { 14 | apiHost: env.data.SANITY_API_HOST, 15 | token: env.data.SANITY_API_TOKEN, 16 | apiVersion: 'vX', // vX until generate API ships in GA 17 | perspective: 'raw', 18 | useCdn: false, 19 | } 20 | 21 | if ('SANITY_PROJECT_ID' in env.data) { 22 | clientConfig.projectId = env.data?.SANITY_PROJECT_ID 23 | } 24 | 25 | if ('SANITY_DATASET' in env.data) { 26 | clientConfig.dataset = env.data?.SANITY_DATASET 27 | } 28 | 29 | if (env.data.INTERNAL_REQUEST_TAG_PREFIX) { 30 | clientConfig.requestTagPrefix = env.data.INTERNAL_REQUEST_TAG_PREFIX 31 | } 32 | 33 | if (env.data.INTERNAL_USE_PROJECT_HOSTNAME !== undefined) { 34 | clientConfig.useProjectHostname = env.data.INTERNAL_USE_PROJECT_HOSTNAME 35 | } 36 | 37 | if (env.data.INTERNAL_REQUESTER_HEADERS) { 38 | const requester = baseRequester.clone() 39 | requester.use(headers(env.data.INTERNAL_REQUESTER_HEADERS)) 40 | clientConfig.requester = requester 41 | } 42 | 43 | return clientConfig 44 | } 45 | -------------------------------------------------------------------------------- /src/config/version.ts: -------------------------------------------------------------------------------- 1 | // Generated file - do not edit 2 | export const VERSION = '0.10.1'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 3 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' 4 | import {registerAllPrompts} from './prompts/register.js' 5 | import {registerAllResources} from './resources/register.js' 6 | import {registerAllTools} from './tools/register.js' 7 | import {env} from './config/env.js' 8 | import {VERSION} from './config/version.js' 9 | 10 | const MCP_SERVER_NAME = '@sanity/mcp-server' 11 | 12 | async function initializeServer() { 13 | const server = new McpServer({ 14 | name: MCP_SERVER_NAME, 15 | version: VERSION, 16 | }) 17 | 18 | registerAllTools(server, env.data?.MCP_USER_ROLE) 19 | registerAllPrompts(server) 20 | registerAllResources(server) 21 | 22 | return server 23 | } 24 | 25 | async function main() { 26 | try { 27 | const server = await initializeServer() 28 | const transport = new StdioServerTransport() 29 | await server.connect(transport) 30 | } catch (error) { 31 | console.error('Fatal error:', error) 32 | process.exit(1) 33 | } 34 | } 35 | 36 | main() 37 | -------------------------------------------------------------------------------- /src/prompts/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | 3 | export function registerAllPrompts(_server: McpServer) {} 4 | -------------------------------------------------------------------------------- /src/resources/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | 3 | export function registerAllResources(_server: McpServer) {} 4 | -------------------------------------------------------------------------------- /src/tools/context/getInitialContextTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {outdent} from 'outdent' 3 | import {listEmbeddingsIndicesTool} from '../embeddings/listEmbeddingsTool.js' 4 | import {listReleasesTool} from '../releases/listReleases.js' 5 | import {contextStore} from './store.js' 6 | import {withErrorHandling} from '../../utils/response.js' 7 | import {listWorkspaceSchemasTool} from '../schema/listWorkspaceSchemasTool.js' 8 | import {MCP_INSTRUCTIONS} from './instructions.js' 9 | import {type BaseToolSchema, createToolClient} from '../../utils/tools.js' 10 | 11 | export const GetInitialContextToolParams = z.object({}) 12 | 13 | type Params = z.infer 14 | 15 | export function hasInitialContext(): boolean { 16 | return contextStore.hasInitialContext() 17 | } 18 | 19 | async function tool(_params: Params) { 20 | const client = createToolClient() 21 | const config = client.config() 22 | const configInfo = `Current Sanity Configuration: 23 | - Project ID: ${config.projectId} 24 | - Dataset: ${config.dataset} 25 | - API Version: ${config.apiVersion} 26 | - Using CDN: ${config.useCdn} 27 | - Perspective: ${config.perspective || 'drafts'}` 28 | 29 | if (!config.projectId || !config.dataset) { 30 | throw new Error('Project ID and Dataset must be set') 31 | } 32 | 33 | const resource: z.infer = { 34 | target: 'dataset', 35 | projectId: config.projectId, 36 | dataset: config.dataset, 37 | } 38 | 39 | const [workspaceSchemas, releases, embeddings] = await Promise.all([ 40 | listWorkspaceSchemasTool({resource}), 41 | listReleasesTool({state: 'active', resource}), 42 | listEmbeddingsIndicesTool({resource}), 43 | ]) 44 | 45 | const todaysDate = new Date().toLocaleDateString('en-US') 46 | 47 | const message = outdent` 48 | ${MCP_INSTRUCTIONS} 49 | 50 | This is the initial context for your Sanity instance: 51 | 52 | 53 | ${configInfo} 54 | 55 | ${workspaceSchemas.content[0].text} 56 | ${embeddings.content[0].text} 57 | ${releases.content[0].text} 58 | 59 | 60 | ${todaysDate} 61 | ` 62 | 63 | contextStore.setInitialContextLoaded() 64 | 65 | return { 66 | content: [ 67 | { 68 | type: 'text' as const, 69 | text: message, 70 | }, 71 | ], 72 | } 73 | } 74 | 75 | export const getInitialContextTool = withErrorHandling(tool, 'Error getting initial context') 76 | -------------------------------------------------------------------------------- /src/tools/context/instructions.ts: -------------------------------------------------------------------------------- 1 | export const MCP_INSTRUCTIONS = `You are a helpful assistant integrated with Sanity through the Model Context Protocol (MCP). 2 | 3 | # Core Agent Principles 4 | 5 | ## IMPORTANT FIRST STEP: 6 | - Always call get_initial_context first to initialize your connection before using any other tools 7 | - This is required for all operations and will give you essential information about the current Sanity environment 8 | 9 | ## Key Principles: 10 | - **Persistence**: Keep going until the user's query is completely resolved. Only end your turn when you are sure the problem is solved. 11 | - **Tool Usage**: If you are not sure about content or schema structure, use your tools to gather relevant information. Do NOT guess or make up answers. 12 | - **Planning**: Plan your approach before each tool call, and reflect on the outcomes of previous tool calls. 13 | - **Resource Clarification**: ALWAYS ask the user which resource to work with if there are multiple resources available. Never assume or guess which resource to use. 14 | - **Error Handling**: NEVER apologize for errors when making tool calls. Instead, immediately try a different approach or tool call. You may briefly inform the user what you're doing, but never say sorry. 15 | 16 | # Content Handling 17 | 18 | ## Schema-First Approach: 19 | - **ALWAYS check the schema first** when users ask about finding or editing specific content types (e.g., "Where can I edit our pricing page?") 20 | - Use get_schema proactively to understand what document types exist before attempting queries 21 | - This prevents failed queries and immediately reveals relevant document types (e.g., discovering a \`pricingPage\` type when asked about pricing) 22 | - Match user requests to the appropriate document types in the schema 23 | - If a user asks to create a type that doesn't match the schema (e.g., "writer" when the schema has "author"), suggest the correct type 24 | 25 | ## Resource Selection: 26 | - When multiple resources are available, ALWAYS explicitly ask the user which resource they want to work with 27 | - Never assume which resource to use, even if one seems more relevant 28 | - Wait for explicit confirmation from the user before performing any operations on a specific resource 29 | - This applies to all operations: querying, creating, updating, or deleting documents 30 | 31 | ## Document Creation Limits: 32 | - A user is only allowed to create/edit/mutate a maximum of 5 (five) documents at a time 33 | - For multiple document creation, use the 'async' parameter (set to true) for better performance 34 | - Only use async=true when creating more than one document in a single conversation 35 | 36 | # Searching for Content 37 | 38 | ## Schema-First Search Strategy: 39 | - **Schema-first approach**: When users ask about specific content (e.g., "pricing page", "blog posts"), use get_schema first to discover relevant document types 40 | - This immediately reveals the correct document types and prevents wasted time on failed queries 41 | - After understanding the schema, use query_documents to search for content based on the correct document types and field names 42 | - If a query returns no results, retry 2-3 times with modified queries by adjusting filters, relaxing constraints, or trying alternative field names 43 | - When retrying queries, consider using more general terms, removing specific filters, or checking for typos in field names 44 | 45 | ## Handling Multi-Step Queries: 46 | - For requests involving related entities (e.g., "Find blog posts by Magnus"), use a multi-step approach 47 | - ALWAYS check the schema structure first to understand document types and relationships 48 | - First, query for the referenced entity (e.g., author) to find its ID or confirm its existence 49 | - If multiple entities match (e.g., several authors named "Magnus"), query them all and display them to the user 50 | - Then use the found ID(s) to query for the primary content (e.g., blog posts referencing that author) 51 | - For references in Sanity, remember to use the proper reference format in GROQ (e.g., \`author._ref == $authorId\`) 52 | - Verify field types in the schema before constructing queries (single reference vs. array of references) 53 | 54 | ## Schema Awareness in Queries: 55 | - **Check the schema BEFORE attempting any content queries** - this is critical for success 56 | - Pay special attention to whether fields are arrays or single values 57 | - For array fields (e.g., \`authors\` containing multiple author references), use array operators: 58 | - Use \`$authorId in authors[]._ref\` for finding documents where an ID exists in an array 59 | - NOT \`authors._ref == $authorId\` which would only work for single references 60 | - For single reference fields (e.g., \`author\` containing one reference), use equality operators: 61 | - Use \`author._ref == $authorId\` for finding documents with a specific reference 62 | - NOT \`$authorId in author[]._ref\` which would cause errors 63 | - Schema checking prevents wasted time on failed queries and immediately reveals the correct approach 64 | - Using the wrong query pattern for the field type will result in no matches 65 | 66 | # Working with GROQ Queries 67 | 68 | ## Query Syntax: 69 | - When writing GROQ queries, pay close attention to syntax, especially for projections 70 | - When creating computed/aliased fields in projections, the field name MUST be a string literal with quotes 71 | - Example of INCORRECT syntax: \*[_type == "author"]{ _id, title: name } 72 | - Example of CORRECT syntax: \*[_type == "author"]{ _id, "title": name } 73 | - Missing quotes around field names in projections will cause "string literal expected" errors 74 | - Always wrap computed field names in double quotes: "fieldName": value 75 | - Regular (non-computed) field selections don't need quotes: _id, name, publishedAt 76 | - Use get_groq_specification for detailed GROQ query syntax help 77 | - Check your queries carefully before submitting them 78 | 79 | ## Text Search: 80 | - For text searching, use the new \`match text::query()\` syntax: 81 | - Basic syntax: \`@ match text::query("foo bar")\` 82 | - Full query example: \`*[_type == "post" && body match text::query("foo bar")]\` 83 | - For exact matches, escape quotes: \`*[_type == "post" && body match text::query("\\"foo bar\\"")]\` 84 | 85 | ## Semantic Search: 86 | - When searching semantically, use semantic_search with appropriate embedding indices 87 | - Use list_embeddings_indices to see available indices for semantic search 88 | - Semantic search is powerful for finding content based on meaning rather than exact text matches 89 | 90 | # Document Operations 91 | 92 | ## Action-First Approach: 93 | - When a user asks you to perform an action (like creating or updating a document), DO IT IMMEDIATELY without just suggesting it 94 | - After performing the action, provide clear confirmation and details 95 | - DO NOT just tell the user "I can help you create/update/delete this" - actually do it using the appropriate tools 96 | 97 | ## Document Management: 98 | - For document creation, use create_document with clear instructions 99 | - Use document_action for operations like publishing, unpublishing, deleting, or discarding documents 100 | - Use update_document for content modifications with AI assistance 101 | - Use patch_document for precise, direct modifications without AI generation (one operation at a time) 102 | - Use transform_document when preserving rich text formatting is crucial 103 | - Use translate_document specifically for language translation tasks 104 | - Use transform_image for AI-powered image operations 105 | - Always verify document existence before attempting to modify it 106 | 107 | ## Document IDs and Formats: 108 | - Draft documents have "drafts." prefix (e.g., "drafts.123abc") 109 | - Published documents have no prefix 110 | - Release documents have "versions.[releaseId]." prefix 111 | 112 | # Releases and Versioning 113 | 114 | ## Release Operations: 115 | - Use list_releases to see available content releases 116 | - Use create_release to create new release packages 117 | - Use edit_release to update release metadata 118 | - Use schedule_release to schedule releases for specific publish times 119 | - Use release_action to publish, archive, unarchive, unschedule, or delete releases 120 | - Use create_version to create versions of documents for specific releases 121 | - Releases provide a way to stage and coordinate content updates 122 | 123 | ## Working with Perspectives: 124 | - Examine available releases and perspectives in the dataset before querying 125 | - Choose the most appropriate perspective: "raw", "drafts", "published", or a release ID 126 | - This ensures you're querying from the correct view of the content 127 | 128 | # Error Handling and Debugging 129 | 130 | ## Error Response Strategy: 131 | - If you encounter an error, explain what went wrong clearly 132 | - Suggest potential solutions or alternatives 133 | - Make sure to check document existence, field requirements, and permission issues 134 | - Try different approaches immediately rather than stopping at the first error 135 | 136 | ## Common Issues to Check: 137 | - Document existence and permissions 138 | - Required field validation 139 | - Proper GROQ syntax (especially projections) 140 | - Correct field types (array vs single reference) 141 | - Schema compliance 142 | 143 | # Response Format and Communication 144 | 145 | ## General Guidelines: 146 | - Keep your responses concise but thorough 147 | - Format complex data for readability using markdown 148 | - Focus on completing the requested tasks efficiently 149 | - Provide context from documents when relevant 150 | - When displaying documents, show the most important fields first 151 | 152 | ## Before Using Tools: 153 | Before running a tool: 154 | 1. Think about what information you need to gather 155 | 2. Determine the right tool and parameters to use 156 | 3. Briefly communicate to the user what you're about to do in a conversational tone 157 | 158 | ## Problem-Solving Strategy: 159 | 1. **Understand the request**: Analyze what the user is asking for and identify necessary document types and fields 160 | 2. **Resource identification**: If multiple resources are available, ALWAYS ask which resource to work with 161 | 3. **Plan your approach**: Determine which tools you'll need and in which order 162 | 4. **Execute with tools**: Use appropriate tools to query, create, or update documents 163 | 5. **Verify results**: Check if results match what the user requested and make adjustments if needed 164 | 6. **Respond clearly**: Present results in a clear, concise format 165 | 166 | # Best Practices 167 | 168 | ## Content Management: 169 | - When creating content, follow the schema structure exactly 170 | - For bulk operations, consider using releases to manage staged content 171 | - Always verify document existence before attempting to modify it 172 | - Remind users that document operations can affect live content 173 | 174 | ## Efficiency Tips: 175 | - Suggest appropriate document types based on user needs 176 | - Recommend efficient ways to structure content 177 | - Explain how Sanity features like references and portable text work 178 | - Help users understand the relationship between schema, documents, and datasets 179 | 180 | ## Tool Selection: 181 | - Use create_document for completely new content with AI generation 182 | - Use update_document for general content updates and AI-powered rewrites 183 | - Use transform_document when preserving rich text formatting is crucial 184 | - Use patch_document for precise, direct modifications (one operation at a time) 185 | - Use translate_document specifically for language translation tasks 186 | - Use transform_image for AI-powered image operations 187 | - Use document_action for publishing, unpublishing, deleting, or discarding documents 188 | - Use query_documents for searching and retrieving content with GROQ 189 | - Use get_schema and list_workspace_schemas for understanding document types and structure 190 | - Use get_groq_specification when you need detailed GROQ syntax help 191 | - Use list_embeddings_indices and semantic_search for AI-powered content discovery 192 | - Use list_projects and get_project_studios for project management 193 | - Use list_datasets, create_dataset, and update_dataset for dataset management 194 | 195 | You have access to powerful tools that can help you work with Sanity effectively. Always start with get_initial_context, check the schema when needed, clarify resources when multiple exist, and take action to complete user requests fully.` 196 | -------------------------------------------------------------------------------- /src/tools/context/middleware.ts: -------------------------------------------------------------------------------- 1 | import {contextStore} from './store.js' 2 | 3 | export function enforceInitialContextMiddleware(toolName: string) { 4 | if (toolName === 'get_initial_context') return 5 | 6 | if (!contextStore.hasInitialContext()) { 7 | throw new Error( 8 | 'Initial context has not been retrieved. Please call get_initial_context tool first to get the initial context.', 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/tools/context/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {getInitialContextTool, GetInitialContextToolParams} from './getInitialContextTool.js' 3 | 4 | export function registerContextTools(server: McpServer) { 5 | server.tool( 6 | 'get_initial_context', 7 | 'IMPORTANT: This tool must be called before using any other tools. It will get initial context and usage instructions for this MCP server. ', 8 | GetInitialContextToolParams.shape, 9 | getInitialContextTool, 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/tools/context/store.ts: -------------------------------------------------------------------------------- 1 | interface InitialContext { 2 | hasGlobalContext: boolean 3 | } 4 | 5 | class ContextStore { 6 | private context: InitialContext = { 7 | hasGlobalContext: false, 8 | } 9 | 10 | setInitialContextLoaded(): void { 11 | this.context.hasGlobalContext = true 12 | } 13 | 14 | hasInitialContext(): boolean { 15 | return this.context.hasGlobalContext 16 | } 17 | 18 | resetInitialContext(): void { 19 | this.context.hasGlobalContext = false 20 | } 21 | } 22 | 23 | export const contextStore = new ContextStore() 24 | -------------------------------------------------------------------------------- /src/tools/datasets/createDatasetTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | 5 | export const CreateDatasetToolParams = BaseToolSchema.extend({ 6 | name: z 7 | .string() 8 | .describe('The name of the dataset (will be automatically formatted to match requirements)'), 9 | aclMode: z.enum(['private', 'public']).optional().describe('The ACL mode for the dataset'), 10 | }) 11 | 12 | type Params = z.infer 13 | 14 | async function tool(args: Params) { 15 | const client = createToolClient(args) 16 | // Only lowercase letters and numbers are allowed 17 | const datasetName = args.name.toLowerCase().replace(/[^a-z0-9]/g, '') 18 | const newDataset = await client.datasets.create(datasetName, { 19 | aclMode: args.aclMode, 20 | }) 21 | 22 | return createSuccessResponse('Dataset created successfully', {newDataset}) 23 | } 24 | 25 | export const createDatasetTool = withErrorHandling(tool, 'Error creating dataset') 26 | -------------------------------------------------------------------------------- /src/tools/datasets/deleteDatasetTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | 5 | export const DeleteDatasetToolParams = BaseToolSchema.extend({ 6 | name: z.string().describe('The name of the dataset to delete'), 7 | }) 8 | 9 | type Params = z.infer 10 | 11 | async function tool(args: Params) { 12 | const client = createToolClient(args) 13 | 14 | const datasets = await client.datasets.list() 15 | const datasetExists = datasets.some((dataset) => dataset.name === args.name) 16 | if (!datasetExists) { 17 | throw new Error(`Dataset '${args.name}' not found. The name has to be exact.`) 18 | } 19 | 20 | await client.datasets.delete(args.name) 21 | 22 | return createSuccessResponse('Dataset deleted successfully', { 23 | deletedDataset: args.name, 24 | }) 25 | } 26 | 27 | export const deleteDatasetTool = withErrorHandling(tool, 'Error deleting dataset') 28 | -------------------------------------------------------------------------------- /src/tools/datasets/listDatasets.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | 5 | export const ListDatasetsToolParams = BaseToolSchema.extend({}) 6 | 7 | type Params = z.infer 8 | 9 | async function tool(params: Params) { 10 | const client = createToolClient(params) 11 | const datasets = await client.datasets.list() 12 | 13 | // Filter out datasets with the 'comments' profile 14 | const filteredDatasets = datasets?.filter((dataset) => dataset.datasetProfile !== 'comments') 15 | if (filteredDatasets.length === 0) { 16 | throw new Error('No datasets found') 17 | } 18 | 19 | const flattenedDatasets: Record = {} 20 | for (const dataset of filteredDatasets) { 21 | flattenedDatasets[dataset.name] = { 22 | name: dataset.name, 23 | aclMode: dataset.aclMode, 24 | createdAt: dataset.createdAt, 25 | } 26 | } 27 | 28 | return createSuccessResponse('Here are the datasets', {datasets: flattenedDatasets}) 29 | } 30 | 31 | export const listDatasetsTool = withErrorHandling(tool, 'Error fetching datasets') 32 | -------------------------------------------------------------------------------- /src/tools/datasets/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {listDatasetsTool, ListDatasetsToolParams} from './listDatasets.js' 3 | import {createDatasetTool, CreateDatasetToolParams} from './createDatasetTool.js' 4 | import {updateDatasetTool, UpdateDatasetToolParams} from './updateDatasetTool.js' 5 | // import {deleteDatasetTool, DeleteDatasetToolParams} from './deleteDatasetTool.js' 6 | 7 | export function registerDatasetsTools(server: McpServer) { 8 | server.tool( 9 | 'list_datasets', 10 | 'Lists all datasets in your Sanity project', 11 | ListDatasetsToolParams.shape, 12 | listDatasetsTool, 13 | ) 14 | server.tool( 15 | 'create_dataset', 16 | 'Creates a new dataset with specified name and access settings', 17 | CreateDatasetToolParams.shape, 18 | createDatasetTool, 19 | ) 20 | server.tool( 21 | 'update_dataset', 22 | "Modifies a dataset's name or access control settings", 23 | UpdateDatasetToolParams.shape, 24 | updateDatasetTool, 25 | ) 26 | // server.tool( 27 | // 'delete_dataset', 28 | // 'Permanently removes a dataset and all its content - use with caution', 29 | // DeleteDatasetToolParams.shape, 30 | // deleteDatasetTool, 31 | // ) 32 | } 33 | -------------------------------------------------------------------------------- /src/tools/datasets/updateDatasetTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | 5 | export const UpdateDatasetToolParams = BaseToolSchema.extend({ 6 | name: z 7 | .string() 8 | .describe('The name of the dataset (will be automatically formatted to match requirements)'), 9 | aclMode: z.enum(['private', 'public']).optional().describe('The ACL mode for the dataset'), 10 | }) 11 | 12 | type Params = z.infer 13 | 14 | async function tool(args: Params) { 15 | const client = createToolClient(args) 16 | const datasets = await client.datasets.list() 17 | const datasetExists = datasets.some((dataset) => dataset.name === args.name) 18 | if (!datasetExists) { 19 | throw new Error(`Dataset '${args.name}' not found. The name has to be exact.`) 20 | } 21 | 22 | const newDataset = await client.datasets.edit(args.name, { 23 | aclMode: args.aclMode, 24 | }) 25 | 26 | return createSuccessResponse('Dataset updated successfully', {newDataset}) 27 | } 28 | 29 | export const updateDatasetTool = withErrorHandling(tool, 'Error updating dataset') 30 | -------------------------------------------------------------------------------- /src/tools/documents/createDocumentTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {randomUUID} from 'node:crypto' 3 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 4 | import {BaseToolSchema, createToolClient, WorkspaceNameSchema} from '../../utils/tools.js' 5 | import {resolveDocumentId, resolveSchemaId} from '../../utils/resolvers.js' 6 | 7 | export const CreateDocumentToolParams = BaseToolSchema.extend({ 8 | _type: z.string().describe('The document type'), 9 | instruction: z.string().describe('Optional instruction for AI to create the document content'), 10 | workspaceName: WorkspaceNameSchema, 11 | releaseId: z 12 | .string() 13 | .optional() 14 | .describe( 15 | 'Optional release ID for creating versioned documents. If provided, the document will be created under the specified release version instead of as a draft', 16 | ), 17 | async: z 18 | .boolean() 19 | .optional() 20 | .default(false) 21 | .describe( 22 | 'Set to true for background processing when creating multiple documents for better performance.', 23 | ), 24 | }) 25 | 26 | type Params = z.infer 27 | 28 | async function tool(params: Params) { 29 | const client = createToolClient(params) 30 | 31 | const documentId = resolveDocumentId(randomUUID(), params.releaseId) 32 | const generateOptions = { 33 | targetDocument: { 34 | operation: 'create', 35 | _id: documentId, 36 | _type: params._type, 37 | }, 38 | instruction: params.instruction, 39 | schemaId: resolveSchemaId(params.workspaceName), 40 | } as const 41 | 42 | if (params.async === true) { 43 | await client.agent.action.generate({ 44 | ...generateOptions, 45 | async: true, 46 | }) 47 | 48 | return createSuccessResponse('Document creation initiated in background', { 49 | success: true, 50 | document: {_id: documentId, _type: params._type}, 51 | }) 52 | } 53 | 54 | const createdDocument = await client.agent.action.generate(generateOptions) 55 | 56 | return createSuccessResponse('Document created successfully', { 57 | success: true, 58 | document: createdDocument, 59 | }) 60 | } 61 | 62 | export const createDocumentTool = withErrorHandling(tool, 'Error creating document') 63 | -------------------------------------------------------------------------------- /src/tools/documents/createVersionTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient, WorkspaceNameSchema} from '../../utils/tools.js' 4 | import {resolveDocumentId, resolveSchemaId} from '../../utils/resolvers.js' 5 | 6 | export const CreateVersionToolParams = BaseToolSchema.extend({ 7 | documentId: z.string().describe('ID of the document to create a version for'), 8 | releaseId: z.string().describe('ID of the release to associate this version with'), 9 | instruction: z 10 | .string() 11 | .optional() 12 | .describe('Optional instruction for AI to modify the document while creating the version'), 13 | workspaceName: WorkspaceNameSchema, 14 | }) 15 | 16 | type Params = z.infer 17 | 18 | async function tool(params: Params) { 19 | const client = createToolClient(params) 20 | 21 | const release = await client.releases.get({releaseId: params.releaseId}) 22 | if (!release) { 23 | throw new Error(`Release with ID '${params.releaseId}' not found`) 24 | } 25 | 26 | const publishedId = resolveDocumentId(params.documentId, false) 27 | const originalDocument = await client.getDocument(publishedId) 28 | if (!originalDocument) { 29 | throw new Error(`Document with ID '${params.documentId}' not found`) 30 | } 31 | 32 | const versionedId = resolveDocumentId(params.documentId, params.releaseId) 33 | 34 | let newDocument = await client.createVersion({ 35 | document: { 36 | ...originalDocument, 37 | _id: versionedId, 38 | }, 39 | releaseId: params.releaseId, 40 | publishedId, 41 | }) 42 | 43 | if (params.instruction) { 44 | newDocument = await client.agent.action.transform({ 45 | schemaId: resolveSchemaId(params.workspaceName), 46 | instruction: params.instruction, 47 | documentId: versionedId, 48 | }) 49 | } 50 | 51 | return createSuccessResponse('Versioned document created successfully', { 52 | success: true, 53 | document: newDocument, 54 | }) 55 | } 56 | 57 | export const createVersionTool = withErrorHandling(tool, 'Error creating document version') 58 | -------------------------------------------------------------------------------- /src/tools/documents/documentActionsTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | import {resolveDocumentId} from '../../utils/resolvers.js' 5 | import {getDraftId} from '@sanity/id-utils' 6 | 7 | const PublishActionSchema = z 8 | .object({ 9 | type: z.literal('publish'), 10 | }) 11 | .describe('Publish a draft document to make it live') 12 | 13 | const UnpublishActionSchema = z 14 | .object({ 15 | type: z.literal('unpublish'), 16 | }) 17 | .describe('Unpublish a published document (moves it back to drafts)') 18 | 19 | const ReplaceVersionActionSchema = z 20 | .object({ 21 | type: z.literal('version.replace'), 22 | releaseId: z.string().describe('ID of the release that contains this document version'), 23 | sourceDocumentId: z.string().describe('ID of the document to copy contents from'), 24 | }) 25 | .describe('Replace the contents of a document version with contents from another document') 26 | 27 | const DiscardVersionActionSchema = z 28 | .object({ 29 | type: z.literal('version.discard'), 30 | releaseId: z.string().describe('ID of the release that contains this document version'), 31 | }) 32 | .describe('Discard a document version from a release (removes it from the release)') 33 | 34 | const UnpublishVersionActionSchema = z 35 | .object({ 36 | type: z.literal('version.unpublish'), 37 | releaseId: z.string().describe('ID of the release that contains this document version'), 38 | }) 39 | .describe('Mark a document to be unpublished when the release is run') 40 | 41 | const DeleteActionSchema = z 42 | .object({ 43 | type: z.literal('delete'), 44 | }) 45 | .describe('Permanently delete a document and all its drafts') 46 | 47 | export const DocumentActionsToolParams = BaseToolSchema.extend({ 48 | id: z.string().describe('ID of the published document'), 49 | action: z 50 | .discriminatedUnion('type', [ 51 | PublishActionSchema, 52 | UnpublishActionSchema, 53 | DiscardVersionActionSchema, 54 | ReplaceVersionActionSchema, 55 | UnpublishVersionActionSchema, 56 | DeleteActionSchema, 57 | ]) 58 | .describe('Type of action to perform on the document'), 59 | }) 60 | 61 | type Params = z.infer 62 | 63 | async function tool(params: Params) { 64 | const client = createToolClient(params) 65 | const publishedId = resolveDocumentId(params.id, false) 66 | const draftId = getDraftId(publishedId) 67 | 68 | switch (params.action.type) { 69 | case 'publish': { 70 | await client.action({ 71 | actionType: 'sanity.action.document.publish', 72 | publishedId, 73 | draftId, 74 | }) 75 | return createSuccessResponse(`Published document '${draftId}' to '${publishedId}'`) 76 | } 77 | 78 | case 'unpublish': { 79 | await client.action({ 80 | actionType: 'sanity.action.document.unpublish', 81 | publishedId, 82 | draftId, 83 | }) 84 | return createSuccessResponse(`Unpublished document '${params.id}' (moved to drafts)`) 85 | } 86 | 87 | case 'version.replace': { 88 | const versionId = resolveDocumentId(publishedId, params.action.releaseId) 89 | const sourceDocument = await client.getDocument(params.action.sourceDocumentId) 90 | 91 | if (!sourceDocument) { 92 | throw new Error(`Source document '${params.action.sourceDocumentId}' not found`) 93 | } 94 | 95 | await client.action({ 96 | actionType: 'sanity.action.document.version.replace', 97 | document: { 98 | ...sourceDocument, 99 | _id: versionId, 100 | }, 101 | }) 102 | return createSuccessResponse( 103 | `Replaced document version '${versionId}' with contents from '${sourceDocument._id}'`, 104 | ) 105 | } 106 | 107 | case 'version.discard': { 108 | const versionId = resolveDocumentId(publishedId, params.action.releaseId) 109 | await client.action({ 110 | actionType: 'sanity.action.document.version.discard', 111 | versionId, 112 | }) 113 | return createSuccessResponse(`Discarded document '${versionId}'`) 114 | } 115 | 116 | case 'version.unpublish': { 117 | const versionId = resolveDocumentId(publishedId, params.action.releaseId) 118 | await client.action({ 119 | actionType: 'sanity.action.document.version.unpublish', 120 | publishedId, 121 | versionId, 122 | }) 123 | return createSuccessResponse( 124 | `Document '${publishedId}' will be unpublished when release '${params.action.releaseId}' is run`, 125 | ) 126 | } 127 | 128 | case 'delete': { 129 | await client.action({ 130 | actionType: 'sanity.action.document.delete', 131 | publishedId, 132 | includeDrafts: [draftId], 133 | }) 134 | return createSuccessResponse(`Deleted document '${params.id}' and all its drafts`) 135 | } 136 | } 137 | } 138 | 139 | export const documentActionsTool = withErrorHandling(tool, 'Error performing document action') 140 | -------------------------------------------------------------------------------- /src/tools/documents/patchDocumentTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient, WorkspaceNameSchema} from '../../utils/tools.js' 4 | import {stringToAgentPath} from '../../utils/path.js' 5 | import {resolveDocumentId, resolveSchemaId} from '../../utils/resolvers.js' 6 | 7 | const SetOperation = z.object({ 8 | op: z.literal('set'), 9 | path: z 10 | .string() 11 | .describe( 12 | 'The path to set. Supports: simple fields ("title"), nested objects ("author.name"), array items by key ("items[_key==\\"item-1\\"]"), and nested properties in arrays ("items[_key==\\"item-1\\"].title")', 13 | ), 14 | value: z 15 | .any() 16 | .describe( 17 | 'The value to set at the specified path. This is an overwriting operation that replaces the full field value.', 18 | ), 19 | }) 20 | 21 | const UnsetOperation = z.object({ 22 | op: z.literal('unset'), 23 | path: z 24 | .string() 25 | .describe( 26 | 'The path to unset. Supports: simple fields ("description"), nested objects ("metadata.keywords"), array items by key ("tags[_key==\\"tag-1\\"]"), and nested properties in arrays ("gallery[_key==\\"img-1\\"].alt")', 27 | ), 28 | }) 29 | 30 | const AppendOperation = z.object({ 31 | op: z.literal('append'), 32 | path: z 33 | .string() 34 | .describe( 35 | 'The path to append to. Supports: simple fields ("categories"), nested arrays ("metadata.tags"), and arrays within keyed items ("sections[_key==\\"sec-1\\"].items"). Can target arrays, strings, text, or numbers.', 36 | ), 37 | value: z 38 | .array(z.unknown()) 39 | .describe( 40 | 'The items to append. Behavior varies by field type: arrays get new items appended, strings get space-separated concatenation, text fields get newline-separated concatenation, numbers get added together.', 41 | ), 42 | }) 43 | 44 | // const MixedOperation = z.object({ 45 | // op: z.literal('mixed'), 46 | // value: z 47 | // .record(z.unknown()) 48 | // .describe( 49 | // 'Object with mixed operations (default behavior). Sets non-array fields and appends to array fields. Use this when you want to update multiple fields with different behaviors in one operation.', 50 | // ), 51 | // }) 52 | 53 | export const PatchDocumentToolParams = BaseToolSchema.extend({ 54 | documentId: z.string().describe('The ID of the document to patch'), 55 | workspaceName: WorkspaceNameSchema, 56 | operation: z 57 | .discriminatedUnion('op', [ 58 | SetOperation, 59 | UnsetOperation, 60 | AppendOperation, 61 | // MixedOperation, 62 | ]) 63 | .describe( 64 | 'Patch operation to apply. Operation is schema-validated and merges with existing data rather than replacing it entirely.', 65 | ), 66 | releaseId: z 67 | .string() 68 | .optional() 69 | .describe( 70 | 'Optional release ID for patching versioned documents. If provided, the document in the specified release will be patched.', 71 | ), 72 | }) 73 | 74 | type Params = z.infer 75 | 76 | async function tool(params: Params) { 77 | const client = createToolClient(params) 78 | 79 | const documentId = resolveDocumentId(params.documentId, params.releaseId) 80 | const document = await client.getDocument(documentId) 81 | if (!document) { 82 | throw new Error(`Document with ID '${documentId}' not found`) 83 | } 84 | 85 | const target = (() => { 86 | switch (params.operation.op) { 87 | case 'set': 88 | return { 89 | path: stringToAgentPath(params.operation.path), 90 | operation: 'set' as const, 91 | value: params.operation.value, 92 | } 93 | case 'unset': 94 | return { 95 | path: stringToAgentPath(params.operation.path), 96 | operation: 'unset' as const, 97 | } 98 | case 'append': 99 | return { 100 | path: stringToAgentPath(params.operation.path), 101 | operation: 'append' as const, 102 | value: params.operation.value, 103 | } 104 | // case 'mixed': 105 | // return { 106 | // path: [], 107 | // operation: 'mixed' as const, 108 | // value: params.operation.value, 109 | // } 110 | } 111 | })() 112 | 113 | const result = await client.agent.action.patch({ 114 | documentId, 115 | schemaId: resolveSchemaId(params.workspaceName), 116 | target, 117 | }) 118 | 119 | return createSuccessResponse('Document patched successfully', { 120 | success: true, 121 | document: result.document, 122 | }) 123 | } 124 | 125 | export const patchDocumentTool = withErrorHandling(tool, 'Error patching document') 126 | -------------------------------------------------------------------------------- /src/tools/documents/queryDocumentsTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {ensureArray, pluralize} from '../../utils/formatters.js' 3 | import {validateGroqQuery} from '../../utils/groq.js' 4 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 5 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 6 | import {tokenLimit, limitByTokens} from '../../utils/tokens.js' 7 | 8 | export const QueryDocumentsToolParams = BaseToolSchema.extend({ 9 | single: z 10 | .boolean() 11 | .optional() 12 | .default(false) 13 | .describe('Whether to return a single document or a list'), 14 | limit: z 15 | .number() 16 | .min(1) 17 | .max(100) 18 | .default(10) 19 | .describe('Maximum number of documents to return (subject to token limits)'), 20 | params: z.record(z.any()).optional().describe('Optional parameters for the GROQ query'), 21 | query: z 22 | .string() 23 | .describe('Complete GROQ query (e.g. "*[_type == \\"post\\"][0...10]{title, _id}")'), 24 | perspective: z 25 | .union([z.enum(['raw', 'drafts', 'published']), z.string()]) 26 | .optional() 27 | .default('raw') 28 | .describe( 29 | 'Optional perspective to query from: "raw", "drafts", "published", or a release ID. Models should examine available releases and perspectives in the dataset before selecting one to ensure they are querying from the most appropriate view of the content.', 30 | ), 31 | }) 32 | 33 | type Params = z.infer 34 | 35 | async function tool(params: Params) { 36 | const validation = await validateGroqQuery(params.query) 37 | if (!validation.isValid) { 38 | throw new Error(`Invalid GROQ query: ${validation.error}`) 39 | } 40 | 41 | const client = createToolClient(params) 42 | const perspectiveClient = client.withConfig({ 43 | perspective: params.perspective ? [params.perspective] : ['raw'], 44 | }) 45 | 46 | const result = await perspectiveClient.fetch(params.query, params.params) 47 | const allDocuments = ensureArray(result) 48 | 49 | const {selectedItems, formattedItems, tokensUsed} = limitByTokens( 50 | allDocuments, 51 | (doc) => JSON.stringify(doc, null, 2), 52 | tokenLimit, 53 | params.limit, 54 | ) 55 | 56 | return createSuccessResponse( 57 | `Query executed successfully. Found ${allDocuments.length} total ${pluralize(allDocuments, 'document')}, returning ${selectedItems.length} (${tokensUsed} tokens)`, 58 | { 59 | documents: {document: formattedItems}, 60 | count: selectedItems.length, 61 | totalAvailable: allDocuments.length, 62 | tokensUsed, 63 | }, 64 | ) 65 | } 66 | 67 | export const queryDocumentsTool = withErrorHandling(tool, 'Error executing GROQ query') 68 | -------------------------------------------------------------------------------- /src/tools/documents/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {queryDocumentsTool, QueryDocumentsToolParams} from './queryDocumentsTool.js' 3 | import {createDocumentTool, CreateDocumentToolParams} from './createDocumentTool.js' 4 | import {updateDocumentTool, UpdateDocumentToolParams} from './updateDocumentTool.js' 5 | import {patchDocumentTool, PatchDocumentToolParams} from './patchDocumentTool.js' 6 | import {transformDocumentTool, TransformDocumentToolParams} from './transformDocumentTool.js' 7 | import {transformImageTool, TransformImageToolParams} from './transformImageTool.js' 8 | import {translateDocumentTool, TranslateDocumentToolParams} from './translateDocumentTool.js' 9 | import {documentActionsTool, DocumentActionsToolParams} from './documentActionsTool.js' 10 | import {createVersionTool, CreateVersionToolParams} from './createVersionTool.js' 11 | 12 | export function registerDocumentsTools(server: McpServer) { 13 | server.tool( 14 | 'create_document', 15 | "Create a completely new document from scratch with AI-generated content. Use when you need to create new content that doesn't exist yet.", 16 | CreateDocumentToolParams.shape, 17 | createDocumentTool, 18 | ) 19 | 20 | server.tool( 21 | 'create_version', 22 | 'Create a version of an existing document for a specific release, with optional AI-generated modifications', 23 | CreateVersionToolParams.shape, 24 | createVersionTool, 25 | ) 26 | 27 | server.tool( 28 | 'transform_image', 29 | 'Transform or generate images in documents using AI. Automatically targets the image asset for transformation or generation. Use "transform" for modifying existing images or "generate" for creating new images.', 30 | TransformImageToolParams.shape, 31 | transformImageTool, 32 | ) 33 | 34 | server.tool( 35 | 'patch_document', 36 | 'Apply precise, direct modifications to document fields without AI generation. Use for exact value changes, adding/removing specific items, or when you know exactly what needs to be changed. No content interpretation or generation. Performs one operation at a time.', 37 | PatchDocumentToolParams.shape, 38 | patchDocumentTool, 39 | ) 40 | 41 | server.tool( 42 | 'update_document', 43 | 'Update existing document content using AI to rewrite, expand, or modify based on natural language instructions. Best for general content updates, rewrites, and improvements where you want AI to interpret and generate new content.', 44 | UpdateDocumentToolParams.shape, 45 | updateDocumentTool, 46 | ) 47 | 48 | server.tool( 49 | 'transform_document', 50 | 'Transform existing content while preserving rich text formatting, annotations, and structure. Use for find-and-replace operations, style corrections, or content modifications where maintaining original formatting is crucial. Choose over "update_document" when formatting preservation is important.', 51 | TransformDocumentToolParams.shape, 52 | transformDocumentTool, 53 | ) 54 | 55 | server.tool( 56 | 'translate_document', 57 | 'Translate document content to a different language while preserving formatting and structure. Specifically designed for language translation with support for protected phrases and style guides. Always use this instead of other tools for translation tasks.', 58 | TranslateDocumentToolParams.shape, 59 | translateDocumentTool, 60 | ) 61 | 62 | server.tool( 63 | 'query_documents', 64 | 'Query documents from Sanity using GROQ query language', 65 | QueryDocumentsToolParams.shape, 66 | queryDocumentsTool, 67 | ) 68 | 69 | server.tool( 70 | 'document_action', 71 | 'Perform document actions like publishing, unpublishing, deleting, or discarding documents', 72 | DocumentActionsToolParams.shape, 73 | documentActionsTool, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/tools/documents/transformDocumentTool.ts: -------------------------------------------------------------------------------- 1 | import type {TransformDocument} from '@sanity/client' 2 | import {z} from 'zod' 3 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 4 | import {WorkspaceNameSchema, BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import {stringToAgentPath} from '../../utils/path.js' 6 | import {resolveSchemaId} from '../../utils/resolvers.js' 7 | 8 | const EditTargetSchema = z.object({ 9 | operation: z.literal('edit'), 10 | _id: z.string(), 11 | }) 12 | 13 | const CreateTargetSchema = z.object({ 14 | operation: z.literal('create'), 15 | _id: z.string().optional(), 16 | }) 17 | 18 | const CreateIfNotExistsTargetSchema = z.object({ 19 | operation: z.literal('createIfNotExists'), 20 | _id: z.string(), 21 | }) 22 | 23 | const CreateOrReplaceTargetSchema = z.object({ 24 | operation: z.literal('createOrReplace'), 25 | _id: z.string(), 26 | }) 27 | 28 | const TargetDocumentSchema = z.discriminatedUnion('operation', [ 29 | EditTargetSchema, 30 | CreateTargetSchema, 31 | CreateIfNotExistsTargetSchema, 32 | CreateOrReplaceTargetSchema, 33 | ]) 34 | 35 | export const TransformDocumentToolParams = BaseToolSchema.extend({ 36 | documentId: z.string().describe('The ID of the source document to transform'), 37 | instruction: z.string().describe('Instructions for transforming the document content'), 38 | workspaceName: WorkspaceNameSchema, 39 | paths: z 40 | .array(z.string()) 41 | .optional() 42 | .describe( 43 | 'Optional target field paths for the transformation. If not set, transforms the whole document. Supports: simple fields ("title"), nested objects ("author.name"), array items by key ("items[_key==\"item-1\"]"), and nested properties in arrays ("items[_key==\"item-1\"].title"). ie: ["field", "array[_key==\"key\"]"] where "key" is a json match', 44 | ), 45 | targetDocument: TargetDocumentSchema.optional().describe( 46 | 'Optional target document configuration if you want to transform to a different document', 47 | ), 48 | async: z 49 | .boolean() 50 | .optional() 51 | .default(false) 52 | .describe( 53 | 'Set to true for background processing when transforming multiple documents for better performance.', 54 | ), 55 | instructionParams: z 56 | .record(z.any()) 57 | .optional() 58 | .describe( 59 | 'Dynamic parameters that can be referenced in the instruction using $paramName syntax', 60 | ), 61 | }) 62 | 63 | type Params = z.infer 64 | 65 | async function tool(params: Params) { 66 | const client = createToolClient(params) 67 | // First check if source document exists 68 | const sourceDocument = await client.getDocument(params.documentId) 69 | if (!sourceDocument) { 70 | throw new Error(`Source document with ID '${params.documentId}' not found`) 71 | } 72 | 73 | const transformOptions: TransformDocument = { 74 | documentId: params.documentId, 75 | instruction: params.instruction, 76 | schemaId: resolveSchemaId(params.workspaceName), 77 | target: params.paths 78 | ? params.paths.map((path) => ({path: stringToAgentPath(path)})) 79 | : undefined, 80 | targetDocument: params.targetDocument, 81 | instructionParams: params.instructionParams, 82 | } 83 | 84 | if (params.async === true) { 85 | await client.agent.action.transform({ 86 | ...transformOptions, 87 | async: true, 88 | }) 89 | 90 | return createSuccessResponse('Document transformation initiated in background', { 91 | success: true, 92 | document: {_id: params.documentId}, 93 | }) 94 | } 95 | 96 | const transformedDocument = await client.agent.action.transform(transformOptions) 97 | 98 | return createSuccessResponse('Document transformed successfully', { 99 | success: true, 100 | document: transformedDocument, 101 | }) 102 | } 103 | 104 | export const transformDocumentTool = withErrorHandling(tool, 'Error transforming document') 105 | -------------------------------------------------------------------------------- /src/tools/documents/transformImageTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {WorkspaceNameSchema, BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | import {stringToAgentPath} from '../../utils/path.js' 5 | import {resolveSchemaId} from '../../utils/resolvers.js' 6 | 7 | export const TransformImageToolParams = BaseToolSchema.extend({ 8 | documentId: z.string().describe('The ID of the document containing the image'), 9 | imagePath: z 10 | .string() 11 | .describe( 12 | 'Path to the image field in the document (e.g., "image", "hero.image", "gallery[0].image")', 13 | ), 14 | instruction: z.string().describe('Instructions for transforming or generating the image'), 15 | operation: z 16 | .enum(['transform', 'generate']) 17 | .describe('Operation type: "transform" for existing images, "generate" for new images'), 18 | workspaceName: WorkspaceNameSchema, 19 | }) 20 | 21 | type Params = z.infer 22 | 23 | async function tool(params: Params) { 24 | const client = createToolClient(params) 25 | 26 | const sourceDocument = await client.getDocument(params.documentId) 27 | if (!sourceDocument) { 28 | throw new Error(`Document with ID '${params.documentId}' not found`) 29 | } 30 | 31 | const actionOptions = { 32 | documentId: params.documentId, 33 | instruction: params.instruction, 34 | schemaId: resolveSchemaId(params.workspaceName), 35 | target: {path: [...stringToAgentPath(params.imagePath), 'asset']}, 36 | } 37 | 38 | if (params.operation === 'generate') { 39 | await client.agent.action.generate(actionOptions) 40 | } else { 41 | await client.agent.action.transform(actionOptions) 42 | } 43 | 44 | return createSuccessResponse(`Image ${params.operation}ed successfully`, { 45 | success: true, 46 | }) 47 | } 48 | 49 | export const transformImageTool = withErrorHandling(tool, 'Error transforming image') 50 | -------------------------------------------------------------------------------- /src/tools/documents/translateDocumentTool.ts: -------------------------------------------------------------------------------- 1 | import type {TranslateDocument} from '@sanity/client' 2 | import {z} from 'zod' 3 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 4 | import {WorkspaceNameSchema, BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import {stringToAgentPath} from '../../utils/path.js' 6 | import {resolveSchemaId} from '../../utils/resolvers.js' 7 | 8 | const LanguageSchema = z.object({ 9 | id: z.string().describe('Language identifier (e.g., "en-US", "no", "fr")'), 10 | title: z.string().optional().describe('Human-readable language name'), 11 | }) 12 | 13 | const EditTargetSchema = z.object({ 14 | operation: z.literal('edit'), 15 | _id: z.string(), 16 | }) 17 | 18 | const CreateTargetSchema = z.object({ 19 | operation: z.literal('create'), 20 | }) 21 | 22 | const TargetDocumentSchema = z.discriminatedUnion('operation', [ 23 | EditTargetSchema, 24 | CreateTargetSchema, 25 | ]) 26 | 27 | export const TranslateDocumentToolParams = BaseToolSchema.extend({ 28 | sourceDocument: z.string().describe('The ID of the source document to translate'), 29 | targetDocument: TargetDocumentSchema.optional().describe( 30 | 'Optional target document configuration if you want to translate to a different document', 31 | ), 32 | language: LanguageSchema.describe('Target language to translate to'), 33 | workspaceName: WorkspaceNameSchema, 34 | paths: z 35 | .array(z.string()) 36 | .optional() 37 | .describe( 38 | 'Target field paths for the translation. Specifies fields to translate. Should always be set if you want to translate specific fields. If not set, targets the whole document. ie: ["field", "array[_key==\"key\"]"] where "key" is a json match', 39 | ), 40 | protectedPhrases: z 41 | .array(z.string()) 42 | .optional() 43 | .describe( 44 | 'List of phrases that should not be translated (e.g., brand names like "Nike", company names like "Microsoft", product names, proper nouns, technical terms, etc.)', 45 | ), 46 | async: z 47 | .boolean() 48 | .optional() 49 | .default(false) 50 | .describe( 51 | 'Set to true for background processing when translating multiple documents for better performance.', 52 | ), 53 | }) 54 | 55 | type Params = z.infer 56 | 57 | async function tool(params: Params) { 58 | const client = createToolClient(params) 59 | // First check if source document exists 60 | const sourceDocument = await client.getDocument(params.sourceDocument) 61 | if (!sourceDocument) { 62 | throw new Error(`Source document with ID '${params.sourceDocument}' not found`) 63 | } 64 | 65 | const translateOptions: TranslateDocument = { 66 | documentId: params.sourceDocument, 67 | // Default to creating a new document unless specifically specified 68 | targetDocument: params.targetDocument || {operation: 'create'}, 69 | 70 | languageFieldPath: sourceDocument.language ? ['language'] : undefined, 71 | fromLanguage: sourceDocument.language, 72 | toLanguage: params.language, 73 | schemaId: resolveSchemaId(params.workspaceName), 74 | target: params.paths 75 | ? params.paths.map((path) => ({path: stringToAgentPath(path)})) 76 | : undefined, 77 | protectedPhrases: params.protectedPhrases, 78 | } 79 | 80 | if (params.async === true) { 81 | await client.agent.action.translate({ 82 | ...translateOptions, 83 | async: true, 84 | }) 85 | 86 | return createSuccessResponse('Document translation initiated in background', { 87 | success: true, 88 | document: {_id: params.sourceDocument}, 89 | }) 90 | } 91 | 92 | const translatedDocument = await client.agent.action.translate(translateOptions) 93 | 94 | return createSuccessResponse('Document translated successfully', { 95 | success: true, 96 | document: translatedDocument, 97 | }) 98 | } 99 | 100 | export const translateDocumentTool = withErrorHandling(tool, 'Error translating document') 101 | -------------------------------------------------------------------------------- /src/tools/documents/updateDocumentTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | 4 | import {WorkspaceNameSchema, BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import type {GenerateInstruction} from '@sanity/client' 6 | import {stringToAgentPath} from '../../utils/path.js' 7 | import {resolveDocumentId, resolveSchemaId} from '../../utils/resolvers.js' 8 | 9 | export const UpdateDocumentToolParams = BaseToolSchema.extend({ 10 | documentId: z.string().describe('The ID of the document to update'), 11 | instruction: z.string().describe('Instruction for AI to update the document content'), 12 | workspaceName: WorkspaceNameSchema, 13 | paths: z 14 | .array(z.string()) 15 | .optional() 16 | .describe( 17 | 'Target field paths for the instruction. Specifies fields to update. Should always be set if you want to update specific fields. If not set, targets the whole document. Supports: simple fields ("title"), nested objects ("author.name"), array items by key ("items[_key==\"item-1\"]"), and nested properties in arrays ("items[_key==\"item-1\"].title"). ie: ["field", "array[_key==\"key\"]"] where "key" is a json match', 18 | ), 19 | releaseId: z 20 | .string() 21 | .optional() 22 | .describe( 23 | 'Optional release ID for creating versioned documents. If provided, the document will be created under the specified release version instead of as a draft', 24 | ), 25 | async: z 26 | .boolean() 27 | .optional() 28 | .default(false) 29 | .describe( 30 | 'Set to true for background processing when updating multiple documents for better performance.', 31 | ), 32 | }) 33 | 34 | type Params = z.infer 35 | 36 | async function tool(params: Params) { 37 | const client = createToolClient(params) 38 | const documentId = resolveDocumentId(params.documentId, params.releaseId) 39 | 40 | const instructOptions: GenerateInstruction = { 41 | documentId, 42 | instruction: params.instruction, 43 | schemaId: resolveSchemaId(params.workspaceName), 44 | target: params.paths 45 | ? params.paths.map((path) => ({path: stringToAgentPath(path)})) 46 | : undefined, 47 | } as const 48 | 49 | if (params.async === true) { 50 | await client.agent.action.generate({ 51 | ...instructOptions, 52 | async: true, 53 | }) 54 | 55 | return createSuccessResponse('Document update initiated in background', { 56 | success: true, 57 | document: {_id: params.documentId}, 58 | }) 59 | } 60 | 61 | const updatedDocument = await client.agent.action.generate(instructOptions) 62 | 63 | return createSuccessResponse('Document updated successfully', { 64 | success: true, 65 | document: updatedDocument, 66 | }) 67 | } 68 | 69 | export const updateDocumentTool = withErrorHandling(tool, 'Error updating document') 70 | -------------------------------------------------------------------------------- /src/tools/embeddings/listEmbeddingsTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import type {EmbeddingsIndex} from '../../types/sanity.js' 4 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import {pluralize} from '../../utils/formatters.js' 6 | 7 | export const ListEmbeddingsIndicesToolParams = BaseToolSchema.extend({}) 8 | 9 | type Params = z.infer 10 | 11 | async function tool(params: Params) { 12 | const client = createToolClient(params) 13 | const config = client.config() 14 | 15 | const indices = await client.request({ 16 | uri: `/embeddings-index/${config.dataset}?projectId=${config.projectId}`, 17 | }) 18 | 19 | if (!indices.length) { 20 | throw new Error('No embeddings indices found') 21 | } 22 | 23 | const flattenedIndices: Record = {} 24 | for (const index of indices) { 25 | flattenedIndices[index.indexName] = { 26 | name: index.indexName, 27 | status: index.status, 28 | } 29 | } 30 | 31 | return createSuccessResponse( 32 | `Found ${indices.length} embeddings ${pluralize(indices, 'index', 'indices')}`, 33 | {indices: flattenedIndices}, 34 | ) 35 | } 36 | 37 | export const listEmbeddingsIndicesTool = withErrorHandling( 38 | tool, 39 | 'Error fetching embeddings indices', 40 | ) 41 | -------------------------------------------------------------------------------- /src/tools/embeddings/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {listEmbeddingsIndicesTool, ListEmbeddingsIndicesToolParams} from './listEmbeddingsTool.js' 3 | import {semanticSearchTool, SemanticSearchToolParams} from './semanticSearchTool.js' 4 | 5 | export function registerEmbeddingsTools(server: McpServer) { 6 | server.tool( 7 | 'list_embeddings_indices', 8 | 'List all available embeddings indices for a dataset', 9 | ListEmbeddingsIndicesToolParams.shape, 10 | listEmbeddingsIndicesTool, 11 | ) 12 | server.tool( 13 | 'semantic_search', 14 | 'Perform a semantic search on an embeddings index', 15 | SemanticSearchToolParams.shape, 16 | semanticSearchTool, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/tools/embeddings/semanticSearchTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import type {EmbeddingsQueryResultItem} from '../../types/sanity.js' 4 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import {pluralize} from '../../utils/formatters.js' 6 | import {limitByTokens, tokenLimit} from '../../utils/tokens.js' 7 | 8 | export const SemanticSearchToolParams = BaseToolSchema.extend({ 9 | indexName: z.string().describe('The name of the embeddings index to search'), 10 | query: z.string().describe('The search query to find semantically similar content'), 11 | limit: z 12 | .number() 13 | .min(1) 14 | .max(100) 15 | .default(10) 16 | .describe('Maximum number of results to return (subject to token limits)'), 17 | }) 18 | 19 | type Params = z.infer 20 | 21 | async function tool(params: Params) { 22 | const client = createToolClient(params) 23 | const config = client.config() 24 | 25 | const results = await client.request({ 26 | uri: `/embeddings-index/query/${config.dataset}/${params.indexName}`, 27 | method: 'post', 28 | withCredentials: true, 29 | body: { 30 | query: params.query, 31 | }, 32 | }) 33 | 34 | if (!results || results.length === 0) { 35 | throw new Error('No search results found') 36 | } 37 | 38 | const {selectedItems, tokensUsed} = limitByTokens( 39 | results, 40 | (item, index) => { 41 | return JSON.stringify( 42 | { 43 | rank: index + 1, 44 | type: item.value.type, 45 | documentId: item.value.documentId, 46 | relevance: `${(item.score * 100).toFixed(1)}%`, 47 | }, 48 | null, 49 | 2, 50 | ) 51 | }, 52 | tokenLimit, 53 | params.limit, 54 | ) 55 | 56 | return createSuccessResponse( 57 | `Found ${results.length} semantic search ${pluralize(results, 'result')} for "${params.query}", returning ${selectedItems.length} (${tokensUsed} tokens)`, 58 | { 59 | results: {result: selectedItems}, 60 | count: selectedItems.length, 61 | totalAvailable: results.length, 62 | tokensUsed, 63 | }, 64 | ) 65 | } 66 | 67 | export const semanticSearchTool = withErrorHandling(tool, 'Error performing semantic search') 68 | -------------------------------------------------------------------------------- /src/tools/groq/getGroqSpecification.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {outdent} from 'outdent' 3 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 4 | 5 | const SPECIFICATION = outdent`# GROQ Specification Guide 6 | 7 |
8 | ## GROQ: Graph-Relational Object Queries 9 | 10 | GROQ is a powerful query language designed for JSON-like data structures. It allows you to: 11 | 12 | - Filter and join data from multiple collections without explicit joins 13 | - Shape your results exactly as needed 14 | - Work with references between documents naturally 15 | - Perform aggregation and grouping operations 16 | - Order and slice result sets efficiently 17 |
18 | 19 |
20 | ## Core Query Structure 21 | 22 | ### Dataset Selection 23 | 24 | Start your query by selecting what to search: 25 | 26 | \`\`\`groq 27 | *[_type == 'post'] 28 | \`\`\` 29 | 30 | The \`*\` selects the current dataset (all documents), then filters to only posts. 31 | 32 | ### Filtering 33 | 34 | Use conditions in square brackets to filter documents: 35 | 36 | \`\`\`groq 37 | *[_type == 'post' && publishedAt > '2023-01-01'] 38 | \`\`\` 39 | 40 | This gets all posts published after January 1, 2023. 41 | 42 | ### Projection 43 | 44 | Shape your results using projection objects: 45 | 46 | \`\`\`groq 47 | *[_type == 'post']{ 48 | title, 49 | body, 50 | author 51 | } 52 | \`\`\` 53 | 54 | This returns only the title, body, and author fields from each post. 55 | 56 | ### Following References 57 | 58 | Use the arrow operator (\`->\`) to follow references to other documents: 59 | 60 | \`\`\`groq 61 | *[_type == 'post']{ 62 | title, 63 | 'authorName': author->name 64 | } 65 | \`\`\` 66 | 67 | This gets posts with their authors' names. 68 | 69 | ### Ordering Results 70 | 71 | Sort your results with the order function: 72 | 73 | \`\`\`groq 74 | *[_type == 'post'] | order(publishedAt desc) 75 | \`\`\` 76 | 77 | This gets posts sorted by publish date (newest first). 78 | 79 | ### Limiting Results 80 | 81 | Use slicing to limit the number of results: 82 | 83 | \`\`\`groq 84 | *[_type == 'post'] | order(publishedAt desc)[0...10] 85 | \`\`\` 86 | 87 | This gets the 10 most recent posts. 88 | 89 |
90 | 91 |
92 | ## Operators 93 | 94 | | Operator | Description | Example | 95 | | ---------- | ------------------------------- | ------------------------------------------- | 96 | | \`==\` | Equal to | \`_type == 'post'\` | 97 | | \`!=\` | Not equal to | \`_type != 'page'\` | 98 | | \`>\` | Greater than | \`publishedAt > '2023-01-01'\` | 99 | | \`>=\` | Greater than or equal to | \`views >= 100\` | 100 | | \`<\` | Less than | \`price < 50\` | 101 | | \`<=\` | Less than or equal to | \`stock <= 10\` | 102 | | \`in\` | Check if value exists in array | \`'fiction' in categories\` | 103 | | \`match\` | Check if string matches pattern | \`title match 'coffee*'\` | 104 | | \`&&\` | Logical AND | \`_type == 'post' && published == true\` | 105 | | \`\|\|\` | Logical OR | \`_type == 'post' \|\| _type == 'article'\` | 106 | | \`!\` | Logical NOT | \`!draft\` | 107 | | \`?\` | Conditional (ternary) | \`featured ? title : null\` | 108 | 109 |
110 | 111 |
112 | ## Useful Functions 113 | 114 | - **count()**: Count items in an array 115 | 116 | \`\`\`groq 117 | count(*[_type == 'post']) 118 | \`\`\` 119 | 120 | - **defined()**: Check if a property exists 121 | 122 | \`\`\`groq 123 | *[_type == 'post' && defined(imageUrl)] 124 | \`\`\` 125 | 126 | - **references()**: Check if a document references another 127 | 128 | \`\`\`groq 129 | *[_type == 'post' && references('author-id')] 130 | \`\`\` 131 | 132 | - **order()**: Sort results by a property 133 | \`\`\`groq 134 | *[_type == 'post'] | order(publishedAt desc) 135 | \`\`\` 136 |
137 | 138 |
139 | ## Common Query Examples 140 | 141 | 1. **Get all posts** 142 | 143 | \`\`\`groq 144 | *[_type == 'post'] 145 | \`\`\` 146 | 147 | 2. **Get just the titles of all posts** 148 | 149 | \`\`\`groq 150 | *[_type == 'post'].title 151 | \`\`\` 152 | 153 | 3. **Get posts with their author names** 154 | 155 | \`\`\`groq 156 | *[_type == 'post']{ 157 | title, 158 | 'authorName': author->name 159 | } 160 | \`\`\` 161 | 162 | 4. **Get the 10 most recent posts** 163 | \`\`\`groq 164 | *[_type == 'post'] | order(publishedAt desc)[0...10] 165 | \`\`\` 166 |
167 | 168 |
169 | ## Learning Resources 170 | 171 | - [GROQ Documentation](https://www.sanity.io/docs/groq) 172 | - [GROQ Cheat Sheet](https://www.sanity.io/docs/query-cheat-sheet) 173 | - [Learn GROQ Interactive](https://groq.dev/) 174 | - [GROQ Specification](https://sanity-io.github.io/GROQ/) 175 |
` 176 | 177 | export const GetGroqSpecificationToolParams = z.object({}) 178 | 179 | type Params = z.infer 180 | 181 | async function tool(_params?: Params) { 182 | return createSuccessResponse(SPECIFICATION) 183 | } 184 | 185 | export const getGroqSpecificationTool = withErrorHandling( 186 | tool, 187 | 'Error retrieving GROQ specification', 188 | ) 189 | -------------------------------------------------------------------------------- /src/tools/groq/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {getGroqSpecificationTool, GetGroqSpecificationToolParams} from './getGroqSpecification.js' 3 | 4 | export function registerGroqTools(server: McpServer) { 5 | server.tool( 6 | 'get_groq_specification', 7 | 'Get the GROQ language specification summary', 8 | GetGroqSpecificationToolParams.shape, 9 | getGroqSpecificationTool, 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/tools/projects/getProjectStudiosTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import type {SanityApplication} from '../../types/sanity.js' 4 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import {pluralize} from '../../utils/formatters.js' 6 | 7 | export const GetProjectStudiosToolParams = BaseToolSchema.extend({}) 8 | 9 | type Params = z.infer 10 | 11 | async function tool(args: Params) { 12 | const client = createToolClient(args) 13 | const projectId = client.config().projectId 14 | 15 | if (!projectId) { 16 | throw new Error('A dataset resource is required') 17 | } 18 | 19 | const applications = await client.request({ 20 | uri: `/projects/${projectId}/user-applications`, 21 | }) 22 | 23 | const studios = applications.filter((app) => app.type === 'studio') 24 | 25 | if (studios.length === 0) { 26 | return { 27 | content: [ 28 | { 29 | type: 'text' as const, 30 | text: 'No studio applications found for this project. Studio may be local only.', 31 | }, 32 | ], 33 | } 34 | } 35 | 36 | const studiosList: Record = {} 37 | 38 | for (const studio of studios) { 39 | studiosList[studio.id] = { 40 | url: studio.appHost, 41 | title: studio.title || 'Untitled Studio', 42 | createdAt: studio.createdAt, 43 | } 44 | } 45 | 46 | return createSuccessResponse( 47 | `Found ${studios.length} ${pluralize(studios, 'studio')} for project "${projectId}"`, 48 | { 49 | studiosList, 50 | }, 51 | ) 52 | } 53 | 54 | export const getProjectStudiosTool = withErrorHandling(tool, 'Error fetching studios') 55 | -------------------------------------------------------------------------------- /src/tools/projects/listProjectsTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 4 | import {pluralize} from '../../utils/formatters.js' 5 | 6 | export const ListProjectsToolParams = BaseToolSchema.extend({}) 7 | 8 | type Params = z.infer 9 | 10 | async function tool(params: Params) { 11 | const client = createToolClient(params) 12 | const projects = await client.projects.list() 13 | 14 | if (projects.length === 0) { 15 | throw new Error('No Sanity projects found for your account.') 16 | } 17 | 18 | const projectsByOrganizations: Record = {} 19 | 20 | for (const project of projects) { 21 | const orgId = project.organizationId as string // All projects have organizationId now; client types are incorrect 22 | 23 | if (!projectsByOrganizations[orgId]) { 24 | projectsByOrganizations[orgId] = [] 25 | } 26 | projectsByOrganizations[orgId].push(project) 27 | } 28 | 29 | const projectsGroupedByOrganization = { 30 | orgs: {} as Record>, 31 | } 32 | 33 | for (const [orgId, orgProjects] of Object.entries(projectsByOrganizations)) { 34 | projectsGroupedByOrganization.orgs[`Organization ${orgId}`] = orgProjects.map((project) => ({ 35 | id: project.id, 36 | name: project.displayName, 37 | createdAt: project.createdAt, 38 | members: project.members.length, 39 | })) 40 | } 41 | 42 | return createSuccessResponse( 43 | `Found ${projects.length} ${pluralize(projects, 'project')} in ${Object.keys(projectsGroupedByOrganization.orgs).length} ${pluralize(Object.keys(projectsGroupedByOrganization.orgs).length, 'organization')}`, 44 | projectsGroupedByOrganization, 45 | ) 46 | } 47 | 48 | export const listProjectsTool = withErrorHandling(tool, 'Error fetching Sanity projects') 49 | -------------------------------------------------------------------------------- /src/tools/projects/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {listProjectsTool, ListProjectsToolParams} from './listProjectsTool.js' 3 | import {getProjectStudiosTool, GetProjectStudiosToolParams} from './getProjectStudiosTool.js' 4 | 5 | export function registerProjectsTools(server: McpServer) { 6 | server.tool( 7 | 'list_projects', 8 | 'Lists all Sanity projects associated with your account', 9 | ListProjectsToolParams.shape, 10 | listProjectsTool, 11 | ) 12 | server.tool( 13 | 'get_project_studios', 14 | 'Retrieves all studio applications linked to a specific Sanity project', 15 | GetProjectStudiosToolParams.shape, 16 | getProjectStudiosTool, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/tools/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js' 3 | import {enforceInitialContextMiddleware} from './context/middleware.js' 4 | import {registerContextTools} from './context/register.js' 5 | import {registerDatasetsTools} from './datasets/register.js' 6 | import {registerDocumentsTools} from './documents/register.js' 7 | import {registerEmbeddingsTools} from './embeddings/register.js' 8 | import {registerGroqTools} from './groq/register.js' 9 | import {registerProjectsTools} from './projects/register.js' 10 | import {registerReleasesTools} from './releases/register.js' 11 | import {registerSchemaTools} from './schema/register.js' 12 | import type {McpRole} from '../types/mcp.js' 13 | import type {THIS_IS_FINE} from '../types/any.js' 14 | import type {ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js' 15 | 16 | function createContextCheckingServer(server: McpServer): McpServer { 17 | const originalTool = server.tool 18 | return new Proxy(server, { 19 | get(target, prop) { 20 | if (prop === 'tool') { 21 | return function (this: THIS_IS_FINE, ...args: THIS_IS_FINE) { 22 | const [name, description, schema, annotations, handler] = args 23 | 24 | const wrappedHandler = async ( 25 | args: THIS_IS_FINE, 26 | extra: RequestHandlerExtra, 27 | ) => { 28 | enforceInitialContextMiddleware(name) 29 | return handler(args, extra) 30 | } 31 | 32 | return originalTool.call(this, name, description, schema, annotations, wrappedHandler) 33 | } 34 | } 35 | return (target as THIS_IS_FINE)[prop] 36 | }, 37 | }) 38 | } 39 | 40 | /** 41 | * Register all tools with for the MCP server 42 | */ 43 | function developerTools(server: McpServer) { 44 | const wrappedServer = createContextCheckingServer(server) 45 | 46 | registerContextTools(wrappedServer) 47 | registerGroqTools(wrappedServer) 48 | registerDocumentsTools(wrappedServer) 49 | registerProjectsTools(wrappedServer) 50 | registerSchemaTools(wrappedServer) 51 | registerDatasetsTools(wrappedServer) 52 | registerReleasesTools(wrappedServer) 53 | registerEmbeddingsTools(wrappedServer) 54 | } 55 | 56 | function editorTools(server: McpServer) { 57 | const wrappedServer = createContextCheckingServer(server) 58 | 59 | registerContextTools(wrappedServer) 60 | registerGroqTools(wrappedServer) 61 | registerDocumentsTools(wrappedServer) 62 | registerSchemaTools(wrappedServer) 63 | registerReleasesTools(wrappedServer) 64 | registerEmbeddingsTools(wrappedServer) 65 | } 66 | 67 | function agentTools(server: McpServer) { 68 | registerGroqTools(server) 69 | registerDocumentsTools(server) 70 | registerSchemaTools(server) 71 | registerReleasesTools(server) 72 | registerEmbeddingsTools(server) 73 | registerProjectsTools(server) 74 | } 75 | 76 | export function registerAllTools(server: McpServer, userRole: McpRole = 'developer') { 77 | const toolMap: Record void> = { 78 | developer: developerTools, 79 | editor: editorTools, 80 | internal_agent_role: agentTools, 81 | } 82 | const registerTools = toolMap[userRole] 83 | 84 | registerTools(server) 85 | } 86 | -------------------------------------------------------------------------------- /src/tools/releases/common.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | 3 | /** 4 | * Common schema types used across release tools 5 | */ 6 | export const ReleaseSchemas = { 7 | releaseId: z.string().describe('ID of the release'), 8 | 9 | title: z.string().describe('Title for the release (e.g., "Spring 2025 Product Launch")'), 10 | 11 | description: z.string().describe('Description for the release'), 12 | 13 | releaseType: z 14 | .enum(['asap', 'undecided', 'scheduled']) 15 | .describe('Type of release (asap, undecided, scheduled)'), 16 | 17 | publishDate: z 18 | .string() 19 | .describe( 20 | 'Date can be ISO format (2025-04-04T18:36:00.000Z) or natural language like "in two weeks"', 21 | ), 22 | 23 | state: z 24 | .enum(['active', 'scheduled', 'published', 'archived', 'deleted', 'all']) 25 | .describe('Filter releases by state (active, scheduled, published, archived, deleted, or all)'), 26 | } 27 | -------------------------------------------------------------------------------- /src/tools/releases/createReleaseTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {parseDateString} from '../../utils/dates.js' 3 | import {generateSanityId} from '../../utils/id.js' 4 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 5 | import {ReleaseSchemas} from './common.js' 6 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 7 | 8 | export const CreateReleaseToolParams = BaseToolSchema.extend({ 9 | title: ReleaseSchemas.title, 10 | description: ReleaseSchemas.description.optional(), 11 | releaseType: ReleaseSchemas.releaseType.optional(), 12 | intendedPublishAt: ReleaseSchemas.publishDate.optional(), 13 | }) 14 | 15 | type Params = z.infer 16 | 17 | async function tool(params: Params) { 18 | const client = createToolClient(params) 19 | const releaseId = generateSanityId(8, 'r') 20 | const intendedPublishAt = parseDateString(params.intendedPublishAt) 21 | 22 | await client.action({ 23 | actionType: 'sanity.action.release.create', 24 | releaseId, 25 | metadata: { 26 | title: params.title, 27 | description: params.description, 28 | releaseType: params.releaseType, 29 | ...(intendedPublishAt && { intendedPublishAt }), 30 | }, 31 | }) 32 | 33 | return createSuccessResponse(`Created new release with ID "${releaseId}"`, { 34 | release: { 35 | releaseId, 36 | title: params.title, 37 | description: params.description, 38 | releaseType: params.releaseType, 39 | intendedPublishAt, 40 | }, 41 | }) 42 | } 43 | 44 | export const createReleaseTool = withErrorHandling(tool, 'Error creating release') 45 | -------------------------------------------------------------------------------- /src/tools/releases/editReleaseTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {parseDateString} from '../../utils/dates.js' 3 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 4 | import {ReleaseSchemas} from './common.js' 5 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 6 | 7 | export const EditReleaseToolParams = BaseToolSchema.extend({ 8 | releaseId: ReleaseSchemas.releaseId, 9 | title: ReleaseSchemas.title.optional(), 10 | description: ReleaseSchemas.description.optional(), 11 | releaseType: ReleaseSchemas.releaseType.optional(), 12 | intendedPublishAt: ReleaseSchemas.publishDate 13 | .optional() 14 | .describe('When the release is intended to be published (informational only)'), 15 | }) 16 | type Params = z.infer 17 | 18 | async function tool(params: Params) { 19 | const client = createToolClient(params) 20 | const metadataChanges = {} as Record 21 | if (params.title) metadataChanges.title = params.title 22 | if (params.description) metadataChanges.description = params.description 23 | if (params.releaseType) metadataChanges.releaseType = params.releaseType 24 | 25 | if (params.intendedPublishAt) { 26 | metadataChanges.intendedPublishAt = parseDateString(params.intendedPublishAt) 27 | } 28 | 29 | if (Object.keys(metadataChanges).length === 0) { 30 | throw new Error('No changes provided for the release metadata.') 31 | } 32 | 33 | await client.action({ 34 | actionType: 'sanity.action.release.edit', 35 | releaseId: params.releaseId, 36 | patch: { 37 | set: { 38 | metadata: metadataChanges, 39 | }, 40 | }, 41 | }) 42 | 43 | return createSuccessResponse(`Updated metadata for release '${params.releaseId}'`, { 44 | updated: { 45 | releaseId: params.releaseId, 46 | changes: metadataChanges, 47 | }, 48 | }) 49 | } 50 | 51 | export const editReleaseTool = withErrorHandling(tool, 'Error editing release') 52 | -------------------------------------------------------------------------------- /src/tools/releases/listReleases.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import type {Release} from '../../types/sanity.js' 4 | import {ReleaseSchemas} from './common.js' 5 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 6 | 7 | export const ListReleasesToolParams = BaseToolSchema.extend({ 8 | state: ReleaseSchemas.state.optional().default('active'), 9 | }) 10 | 11 | type Params = z.infer 12 | 13 | const ALL_RELEASES_QUERY = 'releases::all()' 14 | const FILTERED_RELEASES_QUERY = `${ALL_RELEASES_QUERY}[state == $state]` 15 | 16 | async function tool(params: Params) { 17 | const client = createToolClient(params) 18 | const query = params.state === 'all' ? ALL_RELEASES_QUERY : FILTERED_RELEASES_QUERY 19 | const releases = await client.fetch(query, {state: params.state}) 20 | 21 | if (!releases || releases.length === 0) { 22 | throw new Error( 23 | `No releases found${params.state !== 'all' ? ` with state '${params.state}'` : ''}`, 24 | ) 25 | } 26 | 27 | const formattedReleases: Record = {} 28 | 29 | for (const release of releases) { 30 | const releaseId = release.name 31 | 32 | formattedReleases[releaseId] = { 33 | id: releaseId, 34 | title: release.metadata?.title || 'Untitled release', 35 | description: release.metadata?.description, 36 | state: release.state, 37 | releaseType: release.metadata?.releaseType, 38 | createdAt: release._createdAt, 39 | publishAt: release.publishAt, 40 | } 41 | } 42 | 43 | return createSuccessResponse(`Found ${releases.length} releases for state "${params.state}"`, { 44 | releases: formattedReleases, 45 | }) 46 | } 47 | 48 | export const listReleasesTool = withErrorHandling(tool, 'Error listing releases') 49 | -------------------------------------------------------------------------------- /src/tools/releases/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {releaseActionsTool, ReleaseActionsToolParams} from './releaseActionsTool.js' 3 | import {createReleaseTool, CreateReleaseToolParams} from './createReleaseTool.js' 4 | import {editReleaseTool, EditReleaseToolParams} from './editReleaseTool.js' 5 | import {scheduleReleaseTool, ScheduleReleaseToolParams} from './scheduleReleaseTool.js' 6 | import {listReleasesTool, ListReleasesToolParams} from './listReleases.js' 7 | 8 | export function registerReleasesTools(server: McpServer) { 9 | server.tool( 10 | 'list_releases', 11 | 'List content releases in Sanity, optionally filtered by state (active, scheduled, etc)', 12 | ListReleasesToolParams.shape, 13 | listReleasesTool, 14 | ) 15 | 16 | server.tool( 17 | 'create_release', 18 | 'Create a new content release in Sanity with an automatically generated ID', 19 | CreateReleaseToolParams.shape, 20 | createReleaseTool, 21 | ) 22 | 23 | server.tool( 24 | 'edit_release', 25 | 'Update metadata for an existing content release', 26 | EditReleaseToolParams.shape, 27 | editReleaseTool, 28 | ) 29 | 30 | server.tool( 31 | 'schedule_release', 32 | 'Schedule a content release to be published at a specific time', 33 | ScheduleReleaseToolParams.shape, 34 | scheduleReleaseTool, 35 | ) 36 | 37 | server.tool( 38 | 'release_action', 39 | 'Perform basic actions on existing content releases (publish, archive, unschedule, delete)', 40 | ReleaseActionsToolParams.shape, 41 | releaseActionsTool, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/tools/releases/releaseActionsTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {ReleaseSchemas} from './common.js' 4 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | 6 | /* Create, edit and schedule are defined as separate tools */ 7 | export const ReleaseActionTypes = z.enum([ 8 | 'publish', 9 | 'archive', 10 | 'unarchive', 11 | 'unschedule', 12 | 'delete', 13 | ]) 14 | 15 | export const ReleaseActionsToolParams = BaseToolSchema.extend({ 16 | actionType: ReleaseActionTypes.describe('Type of release action to perform'), 17 | releaseId: ReleaseSchemas.releaseId, 18 | }) 19 | 20 | type Params = z.infer 21 | 22 | async function tool(params: Params) { 23 | const {actionType, releaseId} = params 24 | const client = createToolClient(params) 25 | 26 | await client.action({ 27 | actionType: `sanity.action.release.${actionType}`, 28 | releaseId, 29 | }) 30 | 31 | const actionDescriptionMap = { 32 | publish: `Published all documents in release '${releaseId}'`, 33 | archive: `Archived release '${releaseId}'`, 34 | unarchive: `Unarchived release '${releaseId}'`, 35 | unschedule: `Unscheduled release '${releaseId}'`, 36 | delete: `Permanently deleted release '${releaseId}'`, 37 | } 38 | 39 | return createSuccessResponse(actionDescriptionMap[actionType]) 40 | } 41 | 42 | export const releaseActionsTool = withErrorHandling(tool, 'Error performing release action') 43 | -------------------------------------------------------------------------------- /src/tools/releases/scheduleReleaseTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {parseDateString} from '../../utils/dates.js' 3 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 4 | import {ReleaseSchemas} from './common.js' 5 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 6 | 7 | export const ScheduleReleaseToolParams = BaseToolSchema.extend({ 8 | releaseId: ReleaseSchemas.releaseId, 9 | publishAt: ReleaseSchemas.publishDate, 10 | }) 11 | 12 | type Params = z.infer 13 | 14 | async function tool(params: Params) { 15 | const {releaseId, publishAt} = params 16 | const parsedPublishAt = parseDateString(publishAt) 17 | const client = createToolClient(params) 18 | 19 | if (!parsedPublishAt) { 20 | throw new Error('Invalid publishAt date provided') 21 | } 22 | 23 | await client.action({ 24 | actionType: 'sanity.action.release.schedule', 25 | releaseId, 26 | publishAt: parsedPublishAt, 27 | }) 28 | 29 | return createSuccessResponse( 30 | `Scheduled release '${releaseId}' for publishing at ${parsedPublishAt}`, 31 | { 32 | scheduled: { 33 | releaseId, 34 | publishAt: parsedPublishAt, 35 | }, 36 | }, 37 | ) 38 | } 39 | 40 | export const scheduleReleaseTool = withErrorHandling(tool, 'Error scheduling release') 41 | -------------------------------------------------------------------------------- /src/tools/schema/common.ts: -------------------------------------------------------------------------------- 1 | import {outdent} from 'outdent' 2 | 3 | export const SCHEMA_DEPLOYMENT_INSTRUCTIONS = outdent` 4 | Your Sanity schema has not been deployed. In your Sanity project, run the following command: 5 | \`\`\`shell 6 | npx sanity@latest schema deploy 7 | \`\`\` 8 | ` 9 | -------------------------------------------------------------------------------- /src/tools/schema/getSchemaTool.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import type {ManifestSchemaType} from '../../types/manifest.js' 3 | import {formatSchema} from '../../utils/schema.js' 4 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 5 | import {SCHEMA_DEPLOYMENT_INSTRUCTIONS} from './common.js' 6 | import {WorkspaceNameSchema, BaseToolSchema, createToolClient} from '../../utils/tools.js' 7 | import {resolveSchemaId} from '../../utils/resolvers.js' 8 | 9 | export const GetSchemaToolParams = BaseToolSchema.extend({ 10 | workspaceName: WorkspaceNameSchema, 11 | type: z 12 | .string() 13 | .optional() 14 | .describe( 15 | 'Optional: Specific type name to fetch. If not provided, returns the full schema without detailed field definitions. Full field definitions are only available when requesting a specific type.', 16 | ), 17 | }) 18 | 19 | type Params = z.infer 20 | 21 | async function tool(params: Params) { 22 | const client = createToolClient(params) 23 | const schemaId = resolveSchemaId(params.workspaceName) 24 | const schemaDoc = await client.fetch('*[_id == $schemaId][0]', {schemaId}) 25 | 26 | if (!schemaDoc?.schema) { 27 | throw new Error(SCHEMA_DEPLOYMENT_INSTRUCTIONS) 28 | } 29 | 30 | let schema = JSON.parse(schemaDoc.schema) as ManifestSchemaType[] 31 | 32 | if (params.type && params.type.trim() !== '') { 33 | const typeSchema = schema.filter((type) => type.name === params.type) 34 | if (typeSchema.length === 0) { 35 | throw new Error(`Type "${params.type}" not found in schema`) 36 | } 37 | schema = typeSchema 38 | } 39 | const hasType = Boolean(params.type) // Skip full field definitions if no type specified to avoid blowing up the context window 40 | return createSuccessResponse(formatSchema(schema, schemaId, {lite: hasType === false})) 41 | } 42 | 43 | export const getSchemaTool = withErrorHandling(tool, 'Error fetching schema overview') 44 | -------------------------------------------------------------------------------- /src/tools/schema/listWorkspaceSchemasTool.ts: -------------------------------------------------------------------------------- 1 | import type {z} from 'zod' 2 | import {createSuccessResponse, withErrorHandling} from '../../utils/response.js' 3 | import {SCHEMA_DEPLOYMENT_INSTRUCTIONS} from './common.js' 4 | import {BaseToolSchema, createToolClient} from '../../utils/tools.js' 5 | import type {SchemaId} from '../../types/sanity.js' 6 | import {pluralize} from '../../utils/formatters.js' 7 | 8 | export const SCHEMA_TYPE = 'system.schema' 9 | 10 | export const SCHEMA_ID_PREFIX: SchemaId = '_.schemas.' 11 | export const DEFAULT_SCHEMA_ID: SchemaId = `${SCHEMA_ID_PREFIX}.default` 12 | 13 | export const ListWorkspaceSchemasTool = BaseToolSchema.extend({}) 14 | 15 | type Params = z.infer 16 | 17 | async function tool(params: Params) { 18 | const client = createToolClient(params) 19 | const schemas = await client.fetch<{_id: string}[]>('*[_type == $schemaType]{ _id }', { 20 | schemaType: SCHEMA_TYPE, 21 | }) 22 | 23 | if (!schemas || schemas.length === 0) { 24 | throw new Error(SCHEMA_DEPLOYMENT_INSTRUCTIONS) 25 | } 26 | 27 | return createSuccessResponse(`Found ${schemas.length} ${pluralize(schemas, 'workspace')}`, { 28 | workspaceNames: schemas.map((schema) => { 29 | // Extract only the last part of id. 30 | return schema._id.substring(SCHEMA_ID_PREFIX.length) 31 | }), 32 | }) 33 | } 34 | 35 | export const listWorkspaceSchemasTool = withErrorHandling( 36 | tool, 37 | 'Error fetching available schema IDs', 38 | ) 39 | -------------------------------------------------------------------------------- /src/tools/schema/register.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {getSchemaTool, GetSchemaToolParams} from './getSchemaTool.js' 3 | import {listWorkspaceSchemasTool, ListWorkspaceSchemasTool} from './listWorkspaceSchemasTool.js' 4 | 5 | export function registerSchemaTools(server: McpServer) { 6 | server.tool( 7 | 'get_schema', 8 | 'Get the full schema of the current Sanity workspace', 9 | GetSchemaToolParams.shape, 10 | getSchemaTool, 11 | ) 12 | 13 | server.tool( 14 | 'list_workspace_schemas', 15 | 'Get a list of all available workspace schema names', 16 | ListWorkspaceSchemasTool.shape, 17 | listWorkspaceSchemasTool, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/types/any.ts: -------------------------------------------------------------------------------- 1 | export type THIS_IS_FINE = any 2 | -------------------------------------------------------------------------------- /src/types/manifest.ts: -------------------------------------------------------------------------------- 1 | export type ManifestSerializable = 2 | | string 3 | | number 4 | | boolean 5 | | {[k: string]: ManifestSerializable} 6 | | ManifestSerializable[] 7 | 8 | export type SchemaFile = `${string}.create-schema.json` 9 | 10 | export interface StudioManifest { 11 | version: 1 12 | createdAt: string 13 | workspaces: SerializedManifestWorkspace[] 14 | } 15 | 16 | export interface SerializedManifestWorkspace { 17 | name: string 18 | title: string 19 | subtitle?: string 20 | basePath: `/${string}` 21 | dataset: string 22 | schema: SchemaFile 23 | } 24 | 25 | export interface ManifestSchemaType { 26 | type: string 27 | name: string 28 | title?: string 29 | deprecated?: { 30 | reason: string 31 | } 32 | readOnly?: boolean | 'conditional' 33 | hidden?: boolean | 'conditional' 34 | validation?: ManifestValidationGroup[] 35 | fields?: ManifestField[] 36 | to?: ManifestReferenceMember[] 37 | of?: ManifestArrayMember[] 38 | preview?: { 39 | select: Record 40 | } 41 | fieldsets?: ManifestFieldset[] 42 | options?: Record 43 | //portable text 44 | marks?: { 45 | annotations?: ManifestArrayMember[] 46 | decorators?: ManifestTitledValue[] 47 | } 48 | lists?: ManifestTitledValue[] 49 | styles?: ManifestTitledValue[] 50 | 51 | // userland (assignable to ManifestSerializable | undefined) 52 | [index: string]: unknown 53 | } 54 | 55 | export interface ManifestFieldset { 56 | name: string 57 | title?: string 58 | [index: string]: ManifestSerializable | undefined 59 | } 60 | 61 | export interface ManifestTitledValue { 62 | value: string 63 | title?: string 64 | } 65 | 66 | export type ManifestField = ManifestSchemaType & {fieldset?: string} 67 | export type ManifestArrayMember = Omit & { 68 | name?: string 69 | } 70 | export type ManifestReferenceMember = Omit & { 71 | name?: string 72 | } 73 | 74 | export interface ManifestValidationGroup { 75 | rules: ManifestValidationRule[] 76 | message?: string 77 | level?: 'error' | 'warning' | 'info' 78 | } 79 | 80 | export type ManifestValidationRule = { 81 | flag: 'presence' | string 82 | constraint?: 'required' | ManifestSerializable 83 | [index: string]: ManifestSerializable | undefined 84 | } 85 | -------------------------------------------------------------------------------- /src/types/mcp.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | 3 | export const McpRoleSchema = z 4 | .enum(['developer', 'editor', 'internal_agent_role']) 5 | .default('developer') 6 | 7 | export type McpRole = z.infer 8 | -------------------------------------------------------------------------------- /src/types/sanity.ts: -------------------------------------------------------------------------------- 1 | export interface EmbeddingsIndex { 2 | status: string 3 | indexName: string 4 | projectId: string 5 | dataset: string 6 | projection: string 7 | filter: string 8 | createdAt: string 9 | updatedAt: string 10 | failedDocumentCount: number 11 | startDocumentCount: number 12 | remainingDocumentCount: number 13 | webhookId: string 14 | } 15 | 16 | export interface EmbeddingsQueryResultItem { 17 | score: number 18 | value: { 19 | documentId: string 20 | type: string 21 | } 22 | } 23 | 24 | export interface SanityApplication { 25 | id: string 26 | projectId: string 27 | organizationId: string | null 28 | title: string | null 29 | type: string 30 | urlType: string 31 | appHost: string 32 | dashboardStatus: string 33 | createdAt: string 34 | updatedAt: string 35 | activeDeployment: string | null 36 | manifest: string | null 37 | } 38 | 39 | export interface DocumentBase { 40 | _id: string 41 | _type: string 42 | } 43 | 44 | export interface DocumentLike extends DocumentBase { 45 | [key: string]: unknown 46 | } 47 | 48 | export interface ReleaseMetadata { 49 | releaseType?: 'asap' | 'undecided' | 'scheduled' 50 | title?: string 51 | description?: string 52 | intendedPublishAt?: string 53 | } 54 | 55 | export interface Release { 56 | _createdAt: string 57 | _updatedAt: string 58 | _type: 'system.release' 59 | _id: string 60 | _rev: string 61 | name: string 62 | state: 'active' | 'scheduled' | 'published' | 'archived' | 'deleted' 63 | metadata: ReleaseMetadata 64 | publishAt: string | null 65 | finalDocumentStates: Array<{id: string; _key?: string}> | null 66 | userId: string 67 | } 68 | 69 | export type SchemaId = `_.schemas.${string}` 70 | -------------------------------------------------------------------------------- /src/utils/dates.ts: -------------------------------------------------------------------------------- 1 | import {parse as chronoParse} from 'chrono-node' 2 | 3 | /** 4 | * Helper function to parse date strings in various formats 5 | * @param dateString - ISO date string or natural language date description 6 | * @returns Parsed date as ISO string or null if parsing failed 7 | */ 8 | export function parseDateString(dateString: string | undefined): string | null { 9 | if (!dateString) return null 10 | 11 | // First try to parse as ISO date 12 | const isoDate = new Date(dateString) 13 | 14 | // Check if the date is valid by ensuring it's not NaN 15 | // and that the ISO string conversion works properly 16 | if (!Number.isNaN(isoDate.getTime())) { 17 | return isoDate.toISOString() 18 | } 19 | 20 | // Try to parse as natural language using chrono 21 | const chronoResults = chronoParse(dateString, new Date()) 22 | if (chronoResults.length > 0) { 23 | return chronoResults[0].start.date().toISOString() 24 | } 25 | 26 | return null 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import {XMLBuilder} from 'fast-xml-parser' 2 | 3 | /** 4 | * Formats a response by combining a message with structured data. 5 | * Converts the provided object to XML format and appends the message. 6 | */ 7 | export function formatResponse( 8 | message: string | null, 9 | object: Record = {}, 10 | ): string { 11 | const formattedObject: Record = {} 12 | 13 | for (const [key, value] of Object.entries(object)) { 14 | if (!value) continue 15 | formattedObject[key] = value 16 | } 17 | 18 | const builder = new XMLBuilder({ 19 | format: true, 20 | indentBy: ' ', 21 | suppressEmptyNode: true, 22 | ignoreAttributes: false, 23 | processEntities: false, 24 | }) 25 | 26 | const contextString = builder.build(formattedObject) 27 | 28 | return `${message}: \n${contextString}\n` 29 | } 30 | 31 | /** 32 | * Ensures that a value is an array. 33 | */ 34 | export function ensureArray(value: T | T[] | null | undefined): T[] { 35 | if (value === null || value === undefined) { 36 | return [] 37 | } 38 | return Array.isArray(value) ? value : [value] 39 | } 40 | 41 | /** 42 | * Pluralizes a string based on a count. 43 | * Returns singular form for count of 1, plural form otherwise. 44 | */ 45 | export function pluralize(n: number | unknown[], singular: string, plural?: string): string { 46 | const num = Array.isArray(n) ? n.length : n 47 | return num === 1 ? singular : plural || `${singular}s` 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/groq.ts: -------------------------------------------------------------------------------- 1 | import {parse} from 'groq-js' 2 | 3 | export async function validateGroqQuery( 4 | query: string, 5 | ): Promise<{isValid: boolean; error?: string; tree?: ReturnType}> { 6 | try { 7 | const tree = parse(query) 8 | return {isValid: true, tree} 9 | } catch (error) { 10 | return { 11 | isValid: false, 12 | error: error instanceof Error ? error.message : String(error), 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/utils/id.ts: -------------------------------------------------------------------------------- 1 | const ALLOWED_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 2 | 3 | /** 4 | * Generates a valid Sanity document ID 5 | * 6 | * Creates an ID that follows Sanity's requirements: 7 | * - Contains only a-zA-Z0-9 characters 8 | * - Maximum 128 characters 9 | */ 10 | export function generateSanityId(length = 8, prefix = ''): string { 11 | // Ensure length is within valid range, accounting for prefix length 12 | const prefixLength = prefix.length 13 | const availableLength = 128 - prefixLength 14 | const finalLength = Math.min(Math.max(1, length), availableLength) 15 | 16 | // Generate random ID of specified length 17 | let id = prefix 18 | for (let i = 0; i < finalLength; i++) { 19 | id += ALLOWED_CHARS.charAt(Math.floor(Math.random() * ALLOWED_CHARS.length)) 20 | } 21 | 22 | return id 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import {isIndexSegment, isIndexTuple, isKeySegment, type Path} from '@sanity/types' 2 | 3 | export declare type IndexTuple = [number | '', number | ''] 4 | 5 | export declare type KeyedSegment = { 6 | _key: string 7 | } 8 | 9 | export declare type PathSegment = string | number | KeyedSegment | IndexTuple 10 | 11 | export declare type AgentActionPathSegment = string | KeyedSegment 12 | 13 | export declare type AgentActionPath = AgentActionPathSegment[] 14 | 15 | const rePropName = 16 | /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g 17 | const reAgentPropName = 18 | /[^.[\]]+|\[(?:(["'])((?:(?!\1)[^\\]|\\.)*?)\1)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g 19 | const reKeySegment = /_key\s*==\s*['"](.*)['"]/ 20 | 21 | export function stringToPath(path: string): Path { 22 | const segments = path.match(rePropName) 23 | if (!segments || segments.some((s) => !s)) { 24 | throw new Error(`Invalid path string: "${path}"`) 25 | } 26 | 27 | return segments.map(normalizePathSegment) 28 | } 29 | 30 | export function stringToAgentPath(path: string): AgentActionPath { 31 | const segments = path.match(reAgentPropName) 32 | if (!segments || segments.some((s) => !s)) { 33 | throw new Error(`Invalid path string: "${path}"`) 34 | } 35 | 36 | return segments.filter((segment) => !isIndexSegment(segment)).map(normalizeAgentPathSegment) 37 | } 38 | 39 | export function normalizePathSegment(segment: string): PathSegment { 40 | if (isIndexSegment(segment)) { 41 | return normalizeIndexSegment(segment) 42 | } 43 | 44 | if (isKeySegment(segment)) { 45 | return normalizeKeySegment(segment) 46 | } 47 | 48 | if (isIndexTuple(segment)) { 49 | return normalizeIndexTupleSegment(segment) 50 | } 51 | 52 | return segment 53 | } 54 | 55 | export function normalizeAgentPathSegment(segment: string): AgentActionPathSegment { 56 | if (isKeySegment(segment)) { 57 | return normalizeKeySegment(segment) 58 | } 59 | 60 | return segment 61 | } 62 | 63 | export function normalizeIndexSegment(segment: string): PathSegment { 64 | return Number(segment.replace(/[^\d]/g, '')) 65 | } 66 | 67 | /** @internal */ 68 | export function normalizeKeySegment(segment: string): KeyedSegment { 69 | const segments = segment.match(reKeySegment) 70 | if (!segments) { 71 | throw new Error(`Invalid key segment: ${segment}`) 72 | } 73 | 74 | return {_key: segments[1]} 75 | } 76 | 77 | /** @internal */ 78 | export function normalizeIndexTupleSegment(segment: string): IndexTuple { 79 | const [from, to] = segment.split(':').map((seg) => (seg === '' ? seg : Number(seg))) 80 | return [from, to] 81 | } 82 | 83 | export function pathToString(fieldPath: Path) { 84 | let stringPath = '' 85 | for (let i = 0; i < fieldPath.length; i++) { 86 | const segment = fieldPath[i] 87 | if (isKeySegment(segment)) { 88 | stringPath += `[_key=="${segment._key}"]` 89 | } else if (typeof segment === 'number') { 90 | stringPath += `[${segment}]` 91 | } else { 92 | stringPath += (stringPath.length ? '.' : '') + segment 93 | } 94 | } 95 | return stringPath 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentId, 3 | getPublishedId, 4 | getVersionId, 5 | getVersionNameFromId, 6 | isVersionId, 7 | } from '@sanity/id-utils' 8 | 9 | /** 10 | * Resolves the document ID for tool usage 11 | */ 12 | export function resolveDocumentId(documentId: string, releaseId?: string | false) { 13 | const normalizedDocumentId = DocumentId(documentId) 14 | const publishedId = getPublishedId(normalizedDocumentId) 15 | 16 | // If releaseId is explicitly false, return only published id 17 | if (releaseId === false) { 18 | return publishedId 19 | } 20 | 21 | // If documentId is a version ID, derive the version name from it 22 | let versionName = releaseId 23 | if (isVersionId(normalizedDocumentId)) { 24 | versionName = getVersionNameFromId(normalizedDocumentId) 25 | } 26 | 27 | if (versionName) { 28 | return getVersionId(publishedId, versionName) 29 | } 30 | 31 | return publishedId 32 | } 33 | 34 | /** 35 | * Resolves the schema ID for a given workspace. 36 | */ 37 | export function resolveSchemaId(workspaceName = 'default'): string { 38 | return `_.schemas.${workspaceName}` 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/response.ts: -------------------------------------------------------------------------------- 1 | import type {ServerNotification, ServerRequest} from '@modelcontextprotocol/sdk/types.js' 2 | import {isWithinTokenLimit} from 'gpt-tokenizer' 3 | import type {THIS_IS_FINE} from '../types/any.js' 4 | import {formatResponse} from './formatters.js' 5 | import type {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol.js' 6 | import {tokenLimit} from './tokens.js' 7 | 8 | /** 9 | * Creates a standardized success response 10 | */ 11 | export function createSuccessResponse(message: string, data?: Record) { 12 | const text = data ? formatResponse(message, data) : message 13 | const withinTokenLimit = isWithinTokenLimit(text, tokenLimit) 14 | 15 | if (!withinTokenLimit) { 16 | throw new Error( 17 | 'Response exceeds token limit, consider tweaking your tool arguments to reduce output size!', 18 | ) 19 | } 20 | 21 | return { 22 | content: [ 23 | { 24 | type: 'text', 25 | text, 26 | }, 27 | ], 28 | } 29 | } 30 | 31 | /** 32 | * Higher-order function that wraps tool handlers with standardized error handling 33 | */ 34 | export function withErrorHandling>( 35 | handler: ( 36 | params: T, 37 | extra?: RequestHandlerExtra, 38 | ) => Promise, 39 | errorPrefix = 'Error', 40 | ): ( 41 | params: T, 42 | extra?: RequestHandlerExtra, 43 | ) => Promise { 44 | return async (params: T, extra?: RequestHandlerExtra) => { 45 | try { 46 | return await handler(params, extra) 47 | } catch (error: unknown) { 48 | const errorMessage = error instanceof Error ? error.message : String(error) 49 | return { 50 | isError: true, 51 | content: [ 52 | { 53 | type: 'text', 54 | text: `${errorPrefix}: ${errorMessage}`, 55 | }, 56 | ], 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import {XMLBuilder} from 'fast-xml-parser' 2 | import type {ManifestSchemaType, ManifestSerializable} from '../types/manifest.js' 3 | 4 | interface SchemaXmlNode { 5 | [key: string]: 6 | | ManifestSerializable 7 | | undefined 8 | | {[key: string]: ManifestSerializable | undefined} 9 | | Array<{[key: string]: ManifestSerializable | undefined}> 10 | | SchemaXmlNode 11 | | SchemaXmlNode[] 12 | } 13 | 14 | interface FormatOptions { 15 | lite?: boolean 16 | } 17 | 18 | /** 19 | * Format the schema as XML for better parsing by LLMs 20 | */ 21 | export function formatSchema( 22 | schema: ManifestSchemaType[], 23 | schemaId: string, 24 | options?: FormatOptions, 25 | ): string { 26 | // Filter out types that start with "sanity." 27 | const filteredSchema = schema.filter((type) => !type.name?.startsWith('sanity.')) 28 | 29 | // Create a schema overview section 30 | const schemaOverview = { 31 | schemaOverview: { 32 | schemaId, 33 | totalTypes: filteredSchema.length, 34 | typesSummary: { 35 | type: filteredSchema.map((type) => ({ 36 | name: type.name, 37 | type: type.type, 38 | title: type.title, 39 | fieldsCount: type.fields?.length || 0, 40 | description: getTypeDescription(type), 41 | })), 42 | }, 43 | }, 44 | } 45 | 46 | // If lite mode is enabled, only include the overview 47 | const schemaObject = { 48 | sanitySchema: options?.lite 49 | ? {...schemaOverview} 50 | : { 51 | ...schemaOverview, 52 | schemaDetails: { 53 | types: filteredSchema.map(formatTypeAsObject), 54 | }, 55 | }, 56 | } 57 | 58 | const builder = new XMLBuilder({ 59 | format: true, 60 | indentBy: ' ', 61 | suppressEmptyNode: true, 62 | }) 63 | 64 | return builder.build(schemaObject) 65 | } 66 | 67 | /** 68 | * Generate a concise description of a schema type 69 | */ 70 | function getTypeDescription(type: ManifestSchemaType): string { 71 | const parts: string[] = [] 72 | 73 | if (type.type === 'document') { 74 | parts.push('Document type') 75 | } else if (type.type === 'object') { 76 | parts.push('Object type') 77 | } else if (type.type === 'array') { 78 | const ofTypes = type.of?.map((t) => t.type).join(', ') 79 | parts.push(`Array of [${ofTypes || 'unknown'}]`) 80 | } else { 81 | parts.push(`${type.type} type`) 82 | } 83 | 84 | if (type.fields?.length) { 85 | parts.push(`with ${type.fields.length} fields`) 86 | } 87 | 88 | if (type.deprecated) { 89 | parts.push('(DEPRECATED)') 90 | } 91 | 92 | return parts.join(' ') 93 | } 94 | 95 | /** 96 | * Convert a schema type to a plain object structure for XML serialization 97 | */ 98 | function formatTypeAsObject(type: ManifestSchemaType): SchemaXmlNode { 99 | const result: SchemaXmlNode = { 100 | name: type.name, 101 | type: type.type, 102 | } 103 | 104 | if (type.title) { 105 | result.title = type.title 106 | } 107 | 108 | if (type.deprecated) { 109 | result.deprecated = {reason: type.deprecated.reason} 110 | } 111 | 112 | if (type.readOnly !== undefined) { 113 | result.readOnly = type.readOnly 114 | } 115 | 116 | if (type.hidden !== undefined) { 117 | result.hidden = type.hidden 118 | } 119 | 120 | // Document fields 121 | if (type.fields && type.fields.length > 0) { 122 | result.fields = { 123 | field: type.fields.map(formatFieldAsObject), 124 | } 125 | } 126 | 127 | // Fieldsets 128 | if (type.fieldsets && type.fieldsets.length > 0) { 129 | result.fieldsets = { 130 | fieldset: type.fieldsets.map((fieldset) => ({ 131 | name: fieldset.name, 132 | title: fieldset.title, 133 | })), 134 | } 135 | } 136 | 137 | // Array members 138 | if (type.of && type.of.length > 0) { 139 | result.of = { 140 | type: type.of.map(formatArrayMemberAsObject), 141 | } 142 | } 143 | 144 | // References 145 | if (type.to && type.to.length > 0) { 146 | result.to = { 147 | reference: type.to.map(formatArrayMemberAsObject), 148 | } 149 | } 150 | 151 | // Preview config 152 | if (type.preview) { 153 | result.preview = { 154 | select: Object.entries(type.preview.select).map(([key, value]) => ({ 155 | field: key, 156 | path: value, 157 | })), 158 | } 159 | } 160 | 161 | // Portable Text specifics 162 | if (type.marks) { 163 | result.marks = {} 164 | 165 | if (type.marks.annotations) { 166 | result.marks.annotations = { 167 | annotation: type.marks.annotations.map(formatArrayMemberAsObject), 168 | } 169 | } 170 | 171 | if (type.marks.decorators) { 172 | result.marks.decorators = { 173 | decorator: type.marks.decorators.map((dec) => ({ 174 | value: dec.value, 175 | title: dec.title, 176 | })), 177 | } 178 | } 179 | } 180 | 181 | if (type.lists) { 182 | result.lists = { 183 | list: type.lists.map((list) => ({ 184 | value: list.value, 185 | title: list.title, 186 | })), 187 | } 188 | } 189 | 190 | if (type.styles) { 191 | result.styles = { 192 | style: type.styles.map((style) => ({ 193 | value: style.value, 194 | title: style.title, 195 | })), 196 | } 197 | } 198 | 199 | // Options 200 | if (type.options && Object.keys(type.options).length > 0) { 201 | result.options = {} 202 | 203 | // Convert options to a flat structure 204 | for (const [key, value] of Object.entries(type.options)) { 205 | // Convert complex values to strings to avoid XML serialization issues 206 | if (typeof value === 'object' && value !== null) { 207 | result.options[key] = JSON.stringify(value) 208 | } else { 209 | result.options[key] = value 210 | } 211 | } 212 | } 213 | 214 | // Validation 215 | if (type.validation && type.validation.length > 0) { 216 | result.validation = { 217 | rule: type.validation.flatMap((group) => { 218 | return group.rules.map((rule) => ({ 219 | flag: rule.flag, 220 | constraint: 221 | typeof rule.constraint === 'object' ? JSON.stringify(rule.constraint) : rule.constraint, 222 | message: group.message, 223 | level: group.level, 224 | })) 225 | }), 226 | } 227 | } 228 | 229 | return result 230 | } 231 | 232 | /** 233 | * Format a field as an object for XML serialization 234 | */ 235 | function formatFieldAsObject(field: ManifestSchemaType & {fieldset?: string}): SchemaXmlNode { 236 | const result = formatTypeAsObject(field) 237 | 238 | if (field.fieldset) { 239 | result.fieldset = field.fieldset 240 | } 241 | 242 | return result 243 | } 244 | 245 | /** 246 | * Format an array member as an object for XML serialization 247 | */ 248 | function formatArrayMemberAsObject( 249 | member: Omit & {name?: string}, 250 | ): SchemaXmlNode { 251 | return formatTypeAsObject(member as ManifestSchemaType) 252 | } 253 | -------------------------------------------------------------------------------- /src/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import {countTokens} from 'gpt-tokenizer' 2 | import {env} from '../config/env.js' 3 | 4 | export const tokenLimit = env.data?.MAX_TOOL_TOKEN_OUTPUT || 4096 // Default is defined in env schema 5 | 6 | export interface TokenLimitResult { 7 | selectedItems: T[] 8 | formattedItems: string[] 9 | tokensUsed: number 10 | totalAvailable: number 11 | } 12 | 13 | /** 14 | * Limits a collection of items based on token count, selecting as many items as possible 15 | * without exceeding the specified token limit. 16 | */ 17 | export function limitByTokens( 18 | items: T[], 19 | formatter: (item: T, index: number) => string, 20 | limit: number = tokenLimit, 21 | requestedLimit?: number, 22 | ): TokenLimitResult { 23 | let runningTokens = 0 24 | const selectedItems: T[] = [] 25 | const formattedItems: string[] = [] 26 | 27 | const maxItems = requestedLimit ? Math.min(items.length, requestedLimit) : items.length 28 | 29 | for (let i = 0; i < maxItems; i++) { 30 | const item = items[i] 31 | const formattedItem = formatter(item, i) 32 | const itemTokens = countTokens(formattedItem) 33 | 34 | // Add separator tokens if not the first item 35 | const separatorTokens = selectedItems.length > 0 ? countTokens('\n') : 0 36 | 37 | if (runningTokens + itemTokens + separatorTokens > limit && selectedItems.length > 0) { 38 | break 39 | } 40 | 41 | selectedItems.push(item) 42 | formattedItems.push(formattedItem) 43 | runningTokens += itemTokens + separatorTokens 44 | } 45 | 46 | return { 47 | selectedItems, 48 | formattedItems, 49 | tokensUsed: runningTokens, 50 | totalAvailable: items.length, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod' 2 | import {createClient, type SanityClient} from '@sanity/client' 3 | import {requester as baseRequester} from '@sanity/client' 4 | import {headers as headersMiddleware} from 'get-it/middleware' 5 | import {env} from '../config/env.js' 6 | import {getDefaultClientConfig} from '../config/sanity.js' 7 | 8 | /** 9 | * Schema-related constants and schemas 10 | */ 11 | export const WorkspaceNameSchema = z 12 | .string() 13 | .describe( 14 | 'Workspace name derived from the manifest, not document type. Derived from context or listSchemaWorkspacesTool', 15 | ) 16 | 17 | /** 18 | * Resources-related schemas 19 | */ 20 | const DatasetBaseToolSchema = z 21 | .object({ 22 | target: z.literal('dataset').describe('Used when targeting studio resources'), 23 | projectId: z.string().describe('Unique identifier for the project'), 24 | dataset: z.string().describe('Name or identifier of the dataset'), 25 | }) 26 | .describe('Object that represents a studio resource with its associated project and dataset') 27 | 28 | export const BaseToolSchema = z.object({ 29 | resource: DatasetBaseToolSchema, 30 | }) 31 | 32 | /** 33 | * Creates a Sanity client with the correct configuration based on resource parameters 34 | * 35 | * @param params - Tool parameters that may include a resource 36 | * @returns Configured Sanity client 37 | */ 38 | export function createToolClient>( 39 | {resource}: T = {} as T, 40 | ): SanityClient { 41 | const clientConfig = getDefaultClientConfig() 42 | // TODO: Consider removing this clause when MCP oauth is in place 43 | if (env.data?.MCP_USER_ROLE !== 'internal_agent_role') { 44 | return createClient(clientConfig) 45 | } 46 | 47 | if (resource?.target === 'dataset') { 48 | clientConfig.projectId = resource.projectId 49 | clientConfig.dataset = resource.dataset 50 | 51 | // Modify the Host header to be prefixed with the project ID for internal requests 52 | if (env.data.INTERNAL_REQUESTER_HEADERS) { 53 | const requester = baseRequester.clone() 54 | const headerValues = {...env.data.INTERNAL_REQUESTER_HEADERS} 55 | // If headers.Host exists and is not already prefixed with the project ID 56 | if (headerValues.Host && !headerValues.Host.startsWith(`${resource.projectId}.`)) { 57 | headerValues.Host = `${resource.projectId}.${headerValues.Host}` 58 | } 59 | requester.use(headersMiddleware(headerValues)) 60 | clientConfig.requester = requester 61 | } 62 | 63 | return createClient(clientConfig) 64 | } 65 | 66 | return createClient(clientConfig) 67 | } 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | --------------------------------------------------------------------------------