diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 885f916b..de4ea38f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.19" + ".": "0.1.0-alpha.20" } diff --git a/.stats.yml b/.stats.yml index 9467926b..f957592a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-36086d31791dca6112f0c79f44f296d0cdea35df74f4b3b725f86296a51a86fd.yml -openapi_spec_hash: 0ac69304e57ec27533774ed339a44314 -config_hash: 8563083ae1fa5c137476fb1237aa8ea9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-c9cc06ef2a81689b87584b0ed2b2eaecacdf4481bc191b3b98659e7d56c88414.yml +openapi_spec_hash: dbe850db5529faa656cce1da730da5f7 +config_hash: effe8b6aab8f8658232161326e483a5e diff --git a/CHANGELOG.md b/CHANGELOG.md index a613976f..0e1c82d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 0.1.0-alpha.20 (2026-01-07) + +Full Changelog: [v0.1.0-alpha.19...v0.1.0-alpha.20](https://github.com/stainless-api/stainless-api-typescript/compare/v0.1.0-alpha.19...v0.1.0-alpha.20) + +### ⚠ BREAKING CHANGES + +* **mcp:** remove deprecated tool schemes +* **mcp:** **Migration:** To migrate, simply modify the command used to invoke the MCP server. Currently, the only supported tool scheme is code mode. Now, starting the server with just `node /path/to/mcp/server` or `npx package-name` will invoke code tools: changing your command to one of these is likely all you will need to do. + +### Features + +* add openapi target ([1c07f4a](https://github.com/stainless-api/stainless-api-typescript/commit/1c07f4a27ff101b648b41178c89de46332e47f48)) +* **ci:** Add organization-level setting for AI commit messages ([ea9213e](https://github.com/stainless-api/stainless-api-typescript/commit/ea9213eaea7216e096c9fbcb8286a7024fbebb32)) +* **sql:** initial commit ([54595ed](https://github.com/stainless-api/stainless-api-typescript/commit/54595ed3bf6ec49e4a49fb59125eab12a93f2d08)) + + +### Bug Fixes + +* dedent dockerfile ([c273cf7](https://github.com/stainless-api/stainless-api-typescript/commit/c273cf771f9d479526e1e0fe4c0a5d461dff6383)) +* **mcp:** add client instantiation options to code tool ([71cc60e](https://github.com/stainless-api/stainless-api-typescript/commit/71cc60e3a35a4ed5cf7bb44ae17d1033804d9079)) +* **mcp:** pass base url to code tool ([fc5e7d2](https://github.com/stainless-api/stainless-api-typescript/commit/fc5e7d28983fea9e698f29730f2f0f3db4b737d5)) +* production stage ([c1a4349](https://github.com/stainless-api/stainless-api-typescript/commit/c1a4349857b91d2637f01b9420ff5be14f806a15)) + + +### Chores + +* **mcp:** remove deprecated tool schemes ([8b033b8](https://github.com/stainless-api/stainless-api-typescript/commit/8b033b81b2581291ed1cfcbfaa295276080e59a4)) + + +### Refactors + +* **api:** change completed shape ([ec7f479](https://github.com/stainless-api/stainless-api-typescript/commit/ec7f479865b9f619d433cbcecc3980b9f8dd2c44)) + ## 0.1.0-alpha.19 (2025-12-09) Full Changelog: [v0.1.0-alpha.18...v0.1.0-alpha.19](https://github.com/stainless-api/stainless-api-typescript/compare/v0.1.0-alpha.18...v0.1.0-alpha.19) diff --git a/LICENSE b/LICENSE index 3370fde3..543645c2 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Stainless + Copyright 2026 Stainless Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/package.json b/package.json index 26c3a362..05d4102d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stainless-api/sdk", - "version": "0.1.0-alpha.19", + "version": "0.1.0-alpha.20", "description": "The official TypeScript library for the Stainless API", "author": "Stainless ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 8f50b0b6..09844ebd 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -1,55 +1,53 @@ # Dockerfile for Stainless MCP Server - # - # This Dockerfile builds a Docker image for the MCP Server. - # - # To build the image locally: - # docker build -f packages/mcp-server/Dockerfile -t stainlessapi/stainless-mcp:local . - # - # To run the image: - # docker run -i stainlessapi/stainless-mcp:local [OPTIONS] - # - # Common options: - # --tool= Include specific tools - # --resource= Include tools for specific resources - # --operation=read|write Filter by operation type - # --client= Set client compatibility (e.g., claude, cursor) - # --transport= Set transport type (stdio or http) - # - # For a full list of options: - # docker run -i stainlessapi/stainless-mcp:local --help - # - # Note: The MCP server uses stdio transport by default. Docker's -i flag - # enables interactive mode, allowing the container to communicate over stdin/stdout. - - # Build stage - FROM node:20-alpine AS builder +# +# This Dockerfile builds a Docker image for the MCP Server. +# +# To build the image locally: +# docker build -f packages/mcp-server/Dockerfile -t stainlessapi/stainless-mcp:local . +# +# To run the image: +# docker run -i stainlessapi/stainless-mcp:local [OPTIONS] +# +# Common options: +# --tool= Include specific tools +# --resource= Include tools for specific resources +# --operation=read|write Filter by operation type +# --client= Set client compatibility (e.g., claude, cursor) +# --transport= Set transport type (stdio or http) +# +# For a full list of options: +# docker run -i stainlessapi/stainless-mcp:local --help +# +# Note: The MCP server uses stdio transport by default. Docker's -i flag +# enables interactive mode, allowing the container to communicate over stdin/stdout. + +# Build stage +FROM node:20-alpine AS builder # Enable corepack to use pnpm RUN corepack enable && corepack prepare pnpm@latest --activate - # Install bash for build script - RUN apk add --no-cache bash openssl +# Install bash for build script +RUN apk add --no-cache bash openssl - # Set working directory - WORKDIR /build +# Set working directory +WORKDIR /build - # Copy entire repository - COPY . . +# Copy entire repository +COPY . . - # Install all dependencies and build everything - RUN pnpm install --frozen-lockfile && \ - pnpm build +# Install all dependencies and build everything +RUN pnpm install --frozen-lockfile && \ + pnpm build - # Production stage +# Production stage +FROM node:20-alpine - FROM denoland/deno:alpine - RUN apk add --no-cache npm +# Add non-root user +RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 - # Add non-root user - RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 - - # Set working directory - WORKDIR /app +# Set working directory +WORKDIR /app # Copy the built mcp-server preserving directory structure COPY --from=builder /build/packages/mcp-server/dist ./packages/mcp-server/dist @@ -58,24 +56,20 @@ COPY --from=builder /build/packages/mcp-server/node_modules ./packages/mcp-serve # Copy node_modules from root (pnpm hoists dependencies here) COPY --from=builder /build/node_modules ./node_modules - # Copy the built @stainless-api/sdk into node_modules - COPY --from=builder /build/dist ./node_modules/@stainless-api/sdk - - # Change ownership to nodejs user - RUN chown -R nodejs:nodejs /app +# Copy the built @stainless-api/sdk into node_modules +COPY --from=builder /build/dist ./node_modules/@stainless-api/sdk - # Switch to non-root user - USER nodejs +# Change ownership to nodejs user +RUN chown -R nodejs:nodejs /app - # The MCP server uses stdio transport by default - # No exposed ports needed for stdio communication +# Switch to non-root user +USER nodejs - # This is needed for node to run on the deno:alpine image. - # See . - ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib +# The MCP server uses stdio transport by default +# No exposed ports needed for stdio communication - # Set the entrypoint to the MCP server - ENTRYPOINT ["node", "packages/mcp-server/dist/index.js"] +# Set the entrypoint to the MCP server +ENTRYPOINT ["node", "packages/mcp-server/dist/index.js"] - # Allow passing arguments to the MCP server - CMD [] +# Allow passing arguments to the MCP server +CMD [] diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 6f3a7f7e..708b2c39 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -38,7 +38,7 @@ For clients with a configuration JSON, it might look something like this: "mcpServers": { "stainless_api_sdk_api": { "command": "npx", - "args": ["-y", "@stainless-api/sdk-mcp", "--client=claude", "--tools=all"], + "args": ["-y", "@stainless-api/sdk-mcp"], "env": { "STAINLESS_API_KEY": "My API Key", "STAINLESS_PROJECT": "example-project", @@ -72,110 +72,22 @@ environment variables in Claude Code's `.claude.json`, which can be found in you claude mcp add --transport stdio stainless_api_sdk_api --env STAINLESS_API_KEY="Your STAINLESS_API_KEY here." STAINLESS_PROJECT="Your STAINLESS_PROJECT here." -- npx -y @stainless-api/sdk-mcp ``` -## Exposing endpoints to your MCP Client +## Code Mode -There are three ways to expose endpoints as tools in the MCP server: +This MCP server is built on the "Code Mode" tool scheme. In this MCP Server, +your agent will write code against the TypeScript SDK, which will then be executed in an +isolated sandbox. To accomplish this, the server will expose two tools to your agent: -1. Exposing one tool per endpoint, and filtering as necessary -2. Exposing a set of tools to dynamically discover and invoke endpoints from the API -3. Exposing a docs search tool and a code execution tool, allowing the client to write code to be executed against the TypeScript client +- The first tool is a docs search tool, which can be used to generically query for + documentation about your API/SDK. -### Filtering endpoints and tools +- The second tool is a code tool, where the agent can write code against the TypeScript SDK. + The code will be executed in a sandbox environment without web or filesystem access. Then, + anything the code returns or prints will be returned to the agent as the result of the + tool call. -You can run the package on the command line to discover and filter the set of tools that are exposed by the -MCP Server. This can be helpful for large APIs where including all endpoints at once is too much for your AI's -context window. - -You can filter by multiple aspects: - -- `--tool` includes a specific tool by name -- `--resource` includes all tools under a specific resource, and can have wildcards, e.g. `my.resource*` -- `--operation` includes just read (get/list) or just write operations - -### Dynamic tools - -If you specify `--tools=dynamic` to the MCP server, instead of exposing one tool per endpoint in the API, it will -expose the following tools: - -1. `list_api_endpoints` - Discovers available endpoints, with optional filtering by search query -2. `get_api_endpoint_schema` - Gets detailed schema information for a specific endpoint -3. `invoke_api_endpoint` - Executes any endpoint with the appropriate parameters - -This allows you to have the full set of API endpoints available to your MCP Client, while not requiring that all -of their schemas be loaded into context at once. Instead, the LLM will automatically use these tools together to -search for, look up, and invoke endpoints dynamically. However, due to the indirect nature of the schemas, it -can struggle to provide the correct properties a bit more than when tools are imported explicitly. Therefore, -you can opt-in to explicit tools, the dynamic tools, or both. - -See more information with `--help`. - -All of these command-line options can be repeated, combined together, and have corresponding exclusion versions (e.g. `--no-tool`). - -Use `--list` to see the list of available tools, or see below. - -### Code execution - -If you specify `--tools=code` to the MCP server, it will expose just two tools: - -- `search_docs` - Searches the API documentation and returns a list of markdown results -- `execute` - Runs code against the TypeScript client - -This allows the LLM to implement more complex logic by chaining together many API calls without loading -intermediary results into its context window. - -The code execution itself happens in a Deno sandbox that has network access only to the base URL for the API. - -### Specifying the MCP Client - -Different clients have varying abilities to handle arbitrary tools and schemas. - -You can specify the client you are using with the `--client` argument, and the MCP server will automatically -serve tools and schemas that are more compatible with that client. - -- `--client=`: Set all capabilities based on a known MCP client - - - Valid values: `openai-agents`, `claude`, `claude-code`, `cursor` - - Example: `--client=cursor` - -Additionally, if you have a client not on the above list, or the client has gotten better -over time, you can manually enable or disable certain capabilities: - -- `--capability=`: Specify individual client capabilities - - Available capabilities: - - `top-level-unions`: Enable support for top-level unions in tool schemas - - `valid-json`: Enable JSON string parsing for arguments - - `refs`: Enable support for $ref pointers in schemas - - `unions`: Enable support for union types (anyOf) in schemas - - `formats`: Enable support for format validations in schemas (e.g. date-time, email) - - `tool-name-length=N`: Set maximum tool name length to N characters - - Example: `--capability=top-level-unions --capability=tool-name-length=40` - - Example: `--capability=top-level-unions,tool-name-length=40` - -### Examples - -1. Filter for read operations on cards: - -```bash ---resource=cards --operation=read -``` - -2. Exclude specific tools while including others: - -```bash ---resource=cards --no-tool=create_cards -``` - -3. Configure for Cursor client with custom max tool name length: - -```bash ---client=cursor --capability=tool-name-length=40 -``` - -4. Complex filtering with multiple criteria: - -```bash ---resource=cards,accounts --operation=read --tag=kyc --no-tool=create_cards -``` +Using this scheme, agents are capable of performing very complex tasks deterministically +and repeatably. ## Running remotely @@ -202,127 +114,3 @@ A configuration JSON for this server might look like this, assuming the server i } } ``` - -The command-line arguments for filtering tools and specifying clients can also be used as query parameters in the URL. -For example, to exclude specific tools while including others, use the URL: - -``` -http://localhost:3000?resource=cards&resource=accounts&no_tool=create_cards -``` - -Or, to configure for the Cursor client, with a custom max tool name length, use the URL: - -``` -http://localhost:3000?client=cursor&capability=tool-name-length%3D40 -``` - -## Importing the tools and server individually - -```js -// Import the server, generated endpoints, or the init function -import { server, endpoints, init } from "@stainless-api/sdk-mcp/server"; - -// import a specific tool -import createProjects from "@stainless-api/sdk-mcp/tools/projects/create-projects"; - -// initialize the server and all endpoints -init({ server, endpoints }); - -// manually start server -const transport = new StdioServerTransport(); -await server.connect(transport); - -// or initialize your own server with specific tools -const myServer = new McpServer(...); - -// define your own endpoint -const myCustomEndpoint = { - tool: { - name: 'my_custom_tool', - description: 'My custom tool', - inputSchema: zodToJsonSchema(z.object({ a_property: z.string() })), - }, - handler: async (client: client, args: any) => { - return { myResponse: 'Hello world!' }; - }) -}; - -// initialize the server with your custom endpoints -init({ server: myServer, endpoints: [createProjects, myCustomEndpoint] }); -``` - -## Available Tools - -The following tools are available in this MCP server. - -### Resource `projects`: - -- `create_projects` (`write`): Create a new project. -- `retrieve_projects` (`read`): Retrieve a project by name. -- `update_projects` (`write`): Update a project's properties. -- `list_projects` (`read`): List projects in an organization, from oldest to newest. - -### Resource `projects.branches`: - -- `create_projects_branches` (`write`): Create a new branch for a project. - - The branch inherits the config files from the revision pointed to by the - `branch_from` parameter. In addition, if the revision is a branch name, - the branch will also inherit custom code changes from that branch. - -- `retrieve_projects_branches` (`read`): Retrieve a project branch by name. -- `list_projects_branches` (`read`): Retrieve a project branch by name. -- `delete_projects_branches` (`write`): Delete a project branch by name. -- `rebase_projects_branches` (`write`): Rebase a project branch. - - The branch is rebased onto the `base` branch or commit SHA, inheriting - any config and custom code changes. - -- `reset_projects_branches` (`write`): Reset a project branch. - - If `branch` === `main`, the branch is reset to `target_config_sha`. Otherwise, the - branch is reset to `main`. - -### Resource `projects.configs`: - -- `retrieve_projects_configs` (`read`): - Retrieve the configuration files for a given project. -- `guess_projects_configs` (`write`): - Generate suggestions for changes to config files based on an OpenAPI spec. - -### Resource `builds`: - -- `create_builds` (`write`): Create a build, on top of a project branch, against a given input revision. - - The project branch will be modified so that its latest set of config files - points to the one specified by the input revision. - -- `retrieve_builds` (`read`): Retrieve a build by its ID. -- `list_builds` (`read`): List user-triggered builds for a given project. - - An optional revision can be specified to filter by config commit SHA, or - hashes of file contents. - -### Resource `builds.diagnostics`: - -- `list_builds_diagnostics` (`read`): Get the list of diagnostics for a given build. - - If no language targets are specified, diagnostics for all languages are returned. - -### Resource `builds.target_outputs`: - -- `retrieve_builds_target_outputs` (`read`): Retrieve a method to download an output for a given build target. - - If the requested type of output is `source`, and the requested output - method is `url`, a download link to a tarball of the source files is - returned. If the requested output method is `git`, a Git remote, ref, - and access token (if necessary) is returned. - - Otherwise, the possible types of outputs are specific to the requested - target, and the output method _must_ be `url`. See the documentation for - `type` for more information. - -### Resource `orgs`: - -- `retrieve_orgs` (`read`): Retrieve an organization by name. -- `list_orgs` (`read`): List organizations accessible to the current authentication method. diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 707e841d..f049191a 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@stainless-api/sdk-mcp", - "version": "0.1.0-alpha.19", + "version": "0.1.0-alpha.20", "description": "The official MCP Server for the Stainless API", "author": "Stainless ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index b9fc13e9..49241d03 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, ToolCallResult, asTextContentResult } from './tools/types'; +import { McpTool, Metadata, ToolCallResult, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv } from './server'; import { WorkerSuccess } from './code-tool-types'; @@ -13,7 +13,7 @@ import { WorkerSuccess } from './code-tool-types'; * * @param endpoints - The endpoints to include in the list. */ -export async function codeTool() { +export function codeTool(): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -38,10 +38,15 @@ export async function codeTool() { client_envs: JSON.stringify({ STAINLESS_API_KEY: readEnv('STAINLESS_API_KEY'), STAINLESS_PROJECT: readEnv('STAINLESS_PROJECT'), + STAINLESS_BASE_URL: readEnv('STAINLESS_BASE_URL'), }), }, body: JSON.stringify({ project_name: 'stainless-v0', + client_opts: { + project: readEnv('STAINLESS_PROJECT'), + environment: (readEnv('STAINLESS_ENVIRONMENT') || undefined) as any, + }, code, }), }); diff --git a/packages/mcp-server/src/compat.ts b/packages/mcp-server/src/compat.ts deleted file mode 100644 index f84053c7..00000000 --- a/packages/mcp-server/src/compat.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { z } from 'zod'; -import { Endpoint } from './tools'; - -export interface ClientCapabilities { - topLevelUnions: boolean; - validJson: boolean; - refs: boolean; - unions: boolean; - formats: boolean; - toolNameLength: number | undefined; -} - -export const defaultClientCapabilities: ClientCapabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, -}; - -export const ClientType = z.enum(['openai-agents', 'claude', 'claude-code', 'cursor', 'infer']); -export type ClientType = z.infer; - -// Client presets for compatibility -// Note that these could change over time as models get better, so this is -// a best effort. -export const knownClients: Record, ClientCapabilities> = { - 'openai-agents': { - topLevelUnions: false, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }, - claude: { - topLevelUnions: true, - validJson: false, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }, - 'claude-code': { - topLevelUnions: false, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }, - cursor: { - topLevelUnions: false, - validJson: true, - refs: false, - unions: false, - formats: false, - toolNameLength: 50, - }, -}; - -/** - * Attempts to parse strings into JSON objects - */ -export function parseEmbeddedJSON(args: Record, schema: Record) { - let updated = false; - const newArgs: Record = Object.assign({}, args); - - for (const [key, value] of Object.entries(newArgs)) { - if (typeof value === 'string') { - try { - const parsed = JSON.parse(value); - // Only parse if result is a plain object (not array, null, or primitive) - if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { - newArgs[key] = parsed; - updated = true; - } - } catch (e) { - // Not valid JSON, leave as is - } - } - } - - if (updated) { - return newArgs; - } - - return args; -} - -export type JSONSchema = { - type?: string; - properties?: Record; - required?: string[]; - anyOf?: JSONSchema[]; - $ref?: string; - $defs?: Record; - [key: string]: any; -}; - -/** - * Truncates tool names to the specified length while ensuring uniqueness. - * If truncation would cause duplicate names, appends a number to make them unique. - */ -export function truncateToolNames(names: string[], maxLength: number): Map { - if (maxLength <= 0) { - return new Map(); - } - - const renameMap = new Map(); - const usedNames = new Set(); - - const toTruncate = names.filter((name) => name.length > maxLength); - - if (toTruncate.length === 0) { - return renameMap; - } - - const willCollide = - new Set(toTruncate.map((name) => name.slice(0, maxLength - 1))).size < toTruncate.length; - - if (!willCollide) { - for (const name of toTruncate) { - const truncatedName = name.slice(0, maxLength); - renameMap.set(name, truncatedName); - } - } else { - const baseLength = maxLength - 1; - - for (const name of toTruncate) { - const baseName = name.slice(0, baseLength); - let counter = 1; - - while (usedNames.has(baseName + counter)) { - counter++; - } - - const finalName = baseName + counter; - renameMap.set(name, finalName); - usedNames.add(finalName); - } - } - - return renameMap; -} - -/** - * Removes top-level unions from a tool by splitting it into multiple tools, - * one for each variant in the union. - */ -export function removeTopLevelUnions(tool: Tool): Tool[] { - const inputSchema = tool.inputSchema as JSONSchema; - const variants = inputSchema.anyOf; - - if (!variants || !Array.isArray(variants) || variants.length === 0) { - return [tool]; - } - - const defs = inputSchema.$defs || {}; - - return variants.map((variant, index) => { - const variantSchema: JSONSchema = { - ...inputSchema, - ...variant, - type: 'object', - properties: { - ...(inputSchema.properties || {}), - ...(variant.properties || {}), - }, - }; - - delete variantSchema.anyOf; - - if (!variantSchema['description']) { - variantSchema['description'] = tool.description; - } - - const usedDefs = findUsedDefs(variant, defs); - if (Object.keys(usedDefs).length > 0) { - variantSchema.$defs = usedDefs; - } else { - delete variantSchema.$defs; - } - - return { - ...tool, - name: `${tool.name}_${toSnakeCase(variant['title'] || `variant${index + 1}`)}`, - description: variant['description'] || tool.description, - inputSchema: variantSchema, - } as Tool; - }); -} - -function findUsedDefs( - schema: JSONSchema, - defs: Record, - visited: Set = new Set(), -): Record { - const usedDefs: Record = {}; - - if (typeof schema !== 'object' || schema === null) { - return usedDefs; - } - - if (schema.$ref) { - const refParts = schema.$ref.split('/'); - if (refParts[0] === '#' && refParts[1] === '$defs' && refParts[2]) { - const defName = refParts[2]; - const def = defs[defName]; - if (def && !visited.has(schema.$ref)) { - usedDefs[defName] = def; - visited.add(schema.$ref); - Object.assign(usedDefs, findUsedDefs(def, defs, visited)); - visited.delete(schema.$ref); - } - } - return usedDefs; - } - - for (const key in schema) { - if (key !== '$defs' && typeof schema[key] === 'object' && schema[key] !== null) { - Object.assign(usedDefs, findUsedDefs(schema[key] as JSONSchema, defs, visited)); - } - } - - return usedDefs; -} - -// Export for testing -export { findUsedDefs }; - -/** - * Inlines all $refs in a schema, eliminating $defs. - * If a circular reference is detected, the circular property is removed. - */ -export function inlineRefs(schema: JSONSchema): JSONSchema { - if (!schema || typeof schema !== 'object') { - return schema; - } - - const clonedSchema = { ...schema }; - const defs: Record = schema.$defs || {}; - - delete clonedSchema.$defs; - - const result = inlineRefsRecursive(clonedSchema, defs, new Set()); - // The top level can never be null - return result === null ? {} : result; -} - -function inlineRefsRecursive( - schema: JSONSchema, - defs: Record, - refPath: Set, -): JSONSchema | null { - if (!schema || typeof schema !== 'object') { - return schema; - } - - if (Array.isArray(schema)) { - return schema.map((item) => { - const processed = inlineRefsRecursive(item, defs, refPath); - return processed === null ? {} : processed; - }) as JSONSchema; - } - - const result = { ...schema }; - - if ('$ref' in result && typeof result.$ref === 'string') { - if (result.$ref.startsWith('#/$defs/')) { - const refName = result.$ref.split('/').pop() as string; - const def = defs[refName]; - - // If we've already seen this ref in our path, we have a circular reference - if (refPath.has(result.$ref)) { - // For circular references, we completely remove the property - // by returning null. The parent will remove it. - return null; - } - - if (def) { - const newRefPath = new Set(refPath); - newRefPath.add(result.$ref); - - const inlinedDef = inlineRefsRecursive({ ...def }, defs, newRefPath); - - if (inlinedDef === null) { - return { ...result }; - } - - // Merge the inlined definition with the original schema's properties - // but preserve things like description, etc. - const { $ref, ...rest } = result; - return { ...inlinedDef, ...rest }; - } - } - - // Keep external refs as-is - return result; - } - - for (const key in result) { - if (result[key] && typeof result[key] === 'object') { - const processed = inlineRefsRecursive(result[key] as JSONSchema, defs, refPath); - if (processed === null) { - // Remove properties that would cause circular references - delete result[key]; - } else { - result[key] = processed; - } - } - } - - return result; -} - -/** - * Removes anyOf fields from a schema, using only the first variant. - */ -export function removeAnyOf(schema: JSONSchema): JSONSchema { - if (!schema || typeof schema !== 'object') { - return schema; - } - - if (Array.isArray(schema)) { - return schema.map((item) => removeAnyOf(item)) as JSONSchema; - } - - const result = { ...schema }; - - if ('anyOf' in result && Array.isArray(result.anyOf) && result.anyOf.length > 0) { - const firstVariant = result.anyOf[0]; - - if (firstVariant && typeof firstVariant === 'object') { - // Special handling for properties to ensure deep merge - if (firstVariant.properties && result.properties) { - result.properties = { - ...result.properties, - ...(firstVariant.properties as Record), - }; - } else if (firstVariant.properties) { - result.properties = { ...firstVariant.properties }; - } - - for (const key in firstVariant) { - if (key !== 'properties') { - result[key] = firstVariant[key]; - } - } - } - - delete result.anyOf; - } - - for (const key in result) { - if (result[key] && typeof result[key] === 'object') { - result[key] = removeAnyOf(result[key] as JSONSchema); - } - } - - return result; -} - -/** - * Removes format fields from a schema and appends them to the description. - */ -export function removeFormats(schema: JSONSchema, formatsCapability: boolean): JSONSchema { - if (formatsCapability) { - return schema; - } - - if (!schema || typeof schema !== 'object') { - return schema; - } - - if (Array.isArray(schema)) { - return schema.map((item) => removeFormats(item, formatsCapability)) as JSONSchema; - } - - const result = { ...schema }; - - if ('format' in result && typeof result['format'] === 'string') { - const formatStr = `(format: "${result['format']}")`; - - if ('description' in result && typeof result['description'] === 'string') { - result['description'] = `${result['description']} ${formatStr}`; - } else { - result['description'] = formatStr; - } - - delete result['format']; - } - - for (const key in result) { - if (result[key] && typeof result[key] === 'object') { - result[key] = removeFormats(result[key] as JSONSchema, formatsCapability); - } - } - - return result; -} - -/** - * Applies all compatibility transformations to the endpoints based on the provided capabilities. - */ -export function applyCompatibilityTransformations( - endpoints: Endpoint[], - capabilities: ClientCapabilities, -): Endpoint[] { - let transformedEndpoints = [...endpoints]; - - // Handle top-level unions first as this changes tool names - if (!capabilities.topLevelUnions) { - const newEndpoints: Endpoint[] = []; - - for (const endpoint of transformedEndpoints) { - const variantTools = removeTopLevelUnions(endpoint.tool); - - if (variantTools.length === 1) { - newEndpoints.push(endpoint); - } else { - for (const variantTool of variantTools) { - newEndpoints.push({ - ...endpoint, - tool: variantTool, - }); - } - } - } - - transformedEndpoints = newEndpoints; - } - - if (capabilities.toolNameLength) { - const toolNames = transformedEndpoints.map((endpoint) => endpoint.tool.name); - const renameMap = truncateToolNames(toolNames, capabilities.toolNameLength); - - transformedEndpoints = transformedEndpoints.map((endpoint) => ({ - ...endpoint, - tool: { - ...endpoint.tool, - name: renameMap.get(endpoint.tool.name) ?? endpoint.tool.name, - }, - })); - } - - if (!capabilities.refs || !capabilities.unions || !capabilities.formats) { - transformedEndpoints = transformedEndpoints.map((endpoint) => { - let schema = endpoint.tool.inputSchema as JSONSchema; - - if (!capabilities.refs) { - schema = inlineRefs(schema); - } - - if (!capabilities.unions) { - schema = removeAnyOf(schema); - } - - if (!capabilities.formats) { - schema = removeFormats(schema, capabilities.formats); - } - - return { - ...endpoint, - tool: { - ...endpoint.tool, - inputSchema: schema as typeof endpoint.tool.inputSchema, - }, - }; - }); - } - - return transformedEndpoints; -} - -function toSnakeCase(str: string): string { - return str - .replace(/\s+/g, '_') - .replace(/([a-z])([A-Z])/g, '$1_$2') - .toLowerCase(); -} diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 3e96ed0c..fd018b80 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,6 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from './tools/types'; +import { Metadata, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; diff --git a/packages/mcp-server/src/dynamic-tools.ts b/packages/mcp-server/src/dynamic-tools.ts deleted file mode 100644 index f967f213..00000000 --- a/packages/mcp-server/src/dynamic-tools.ts +++ /dev/null @@ -1,159 +0,0 @@ -import Stainless from '@stainless-api/sdk'; -import { Endpoint, asTextContentResult, ToolCallResult } from './tools/types'; -import { zodToJsonSchema } from 'zod-to-json-schema'; -import { z } from 'zod'; -import { Cabidela } from '@cloudflare/cabidela'; - -function zodToInputSchema(schema: z.ZodSchema) { - return { - type: 'object' as const, - ...(zodToJsonSchema(schema) as any), - }; -} - -/** - * A list of tools that expose all the endpoints in the API dynamically. - * - * Instead of exposing every endpoint as its own tool, which uses up too many tokens for LLMs to use at once, - * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then - * a generic endpoint that can be used to invoke any endpoint with the provided arguments. - * - * @param endpoints - The endpoints to include in the list. - */ -export function dynamicTools(endpoints: Endpoint[]): Endpoint[] { - const listEndpointsSchema = z.object({ - search_query: z - .string() - .optional() - .describe( - 'An optional search query to filter the endpoints by. Provide a partial name, resource, operation, or tag to filter the endpoints returned.', - ), - }); - - const listEndpointsTool = { - metadata: { - resource: 'dynamic_tools', - operation: 'read' as const, - tags: [], - }, - tool: { - name: 'list_api_endpoints', - description: 'List or search for all endpoints in the Stainless TypeScript API', - inputSchema: zodToInputSchema(listEndpointsSchema), - }, - handler: async ( - client: Stainless, - args: Record | undefined, - ): Promise => { - const query = args && listEndpointsSchema.parse(args).search_query?.trim(); - - const filteredEndpoints = - query && query.length > 0 ? - endpoints.filter((endpoint) => { - const fieldsToMatch = [ - endpoint.tool.name, - endpoint.tool.description, - endpoint.metadata.resource, - endpoint.metadata.operation, - ...endpoint.metadata.tags, - ]; - return fieldsToMatch.some((field) => field && field.toLowerCase().includes(query.toLowerCase())); - }) - : endpoints; - - return asTextContentResult({ - tools: filteredEndpoints.map(({ tool, metadata }) => ({ - name: tool.name, - description: tool.description, - resource: metadata.resource, - operation: metadata.operation, - tags: metadata.tags, - })), - }); - }, - }; - - const getEndpointSchema = z.object({ - endpoint: z.string().describe('The name of the endpoint to get the schema for.'), - }); - const getEndpointTool = { - metadata: { - resource: 'dynamic_tools', - operation: 'read' as const, - tags: [], - }, - tool: { - name: 'get_api_endpoint_schema', - description: - 'Get the schema for an endpoint in the Stainless TypeScript API. You can use the schema returned by this tool to invoke an endpoint with the `invoke_api_endpoint` tool.', - inputSchema: zodToInputSchema(getEndpointSchema), - }, - handler: async (client: Stainless, args: Record | undefined) => { - if (!args) { - throw new Error('No endpoint provided'); - } - const endpointName = getEndpointSchema.parse(args).endpoint; - - const endpoint = endpoints.find((e) => e.tool.name === endpointName); - if (!endpoint) { - throw new Error(`Endpoint ${endpointName} not found`); - } - return asTextContentResult(endpoint.tool); - }, - }; - - const invokeEndpointSchema = z.object({ - endpoint_name: z.string().describe('The name of the endpoint to invoke.'), - args: z - .record(z.string(), z.any()) - .describe( - 'The arguments to pass to the endpoint. This must match the schema returned by the `get_api_endpoint_schema` tool.', - ), - }); - - const invokeEndpointTool = { - metadata: { - resource: 'dynamic_tools', - operation: 'write' as const, - tags: [], - }, - tool: { - name: 'invoke_api_endpoint', - description: - 'Invoke an endpoint in the Stainless TypeScript API. Note: use the `list_api_endpoints` tool to get the list of endpoints and `get_api_endpoint_schema` tool to get the schema for an endpoint.', - inputSchema: zodToInputSchema(invokeEndpointSchema), - }, - handler: async ( - client: Stainless, - args: Record | undefined, - ): Promise => { - if (!args) { - throw new Error('No endpoint provided'); - } - const { success, data, error } = invokeEndpointSchema.safeParse(args); - if (!success) { - throw new Error(`Invalid arguments for endpoint. ${error?.format()}`); - } - const { endpoint_name, args: endpointArgs } = data; - - const endpoint = endpoints.find((e) => e.tool.name === endpoint_name); - if (!endpoint) { - throw new Error( - `Endpoint ${endpoint_name} not found. Use the \`list_api_endpoints\` tool to get the list of available endpoints.`, - ); - } - - try { - // Try to validate the arguments for a better error message - const cabidela = new Cabidela(endpoint.tool.inputSchema, { fullErrors: true }); - cabidela.validate(endpointArgs); - } catch (error) { - throw new Error(`Invalid arguments for endpoint ${endpoint_name}:\n${error}`); - } - - return await endpoint.handler(client, endpointArgs); - }, - }; - - return [getEndpointTool, listEndpointsTool, invokeEndpointTool]; -} diff --git a/packages/mcp-server/src/filtering.ts b/packages/mcp-server/src/filtering.ts deleted file mode 100644 index eaae0fcf..00000000 --- a/packages/mcp-server/src/filtering.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -import initJq from 'jq-web'; - -export async function maybeFilter(jqFilter: unknown | undefined, response: any): Promise { - if (jqFilter && typeof jqFilter === 'string') { - return await jq(response, jqFilter); - } else { - return response; - } -} - -async function jq(json: any, jqFilter: string) { - return (await initJq).json(json, jqFilter); -} - -export function isJqError(error: any): error is Error { - return error instanceof Error && 'stderr' in error; -} diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 84517003..2366d8f9 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -4,38 +4,21 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; -import { fromError } from 'zod-validation-error/v3'; -import { McpOptions, parseQueryOptions } from './options'; +import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; const newServer = ({ clientOptions, - mcpOptions: defaultMcpOptions, req, res, }: { clientOptions: ClientOptions; - mcpOptions: McpOptions; req: express.Request; res: express.Response; }): McpServer | null => { const server = newMcpServer(); - let mcpOptions: McpOptions; - try { - mcpOptions = parseQueryOptions(defaultMcpOptions, req.query); - } catch (error) { - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Invalid request: ${fromError(error)}`, - }, - }); - return null; - } - try { const authOptions = parseAuthHeaders(req); initMcpServer({ @@ -44,7 +27,6 @@ const newServer = ({ ...clientOptions, ...authOptions, }, - mcpOptions, }); } catch (error) { res.status(401).json({ diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 4850a0e2..0f6dd426 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -1,20 +1,15 @@ #!/usr/bin/env node import { selectTools } from './server'; -import { Endpoint, endpoints } from './tools'; import { McpOptions, parseCLIOptions } from './options'; import { launchStdioServer } from './stdio'; import { launchStreamableHTTPServer } from './http'; +import type { McpTool } from './types'; async function main() { const options = parseOptionsOrError(); - if (options.list) { - listAllTools(); - return; - } - - const selectedTools = await selectToolsOrError(endpoints, options); + const selectedTools = await selectToolsOrError(options); console.error( `MCP Server starting with ${selectedTools.length} tools:`, @@ -23,7 +18,7 @@ async function main() { switch (options.transport) { case 'stdio': - await launchStdioServer(options); + await launchStdioServer(); break; case 'http': await launchStreamableHTTPServer(options, options.port ?? options.socket); @@ -47,9 +42,9 @@ function parseOptionsOrError() { } } -async function selectToolsOrError(endpoints: Endpoint[], options: McpOptions): Promise { +async function selectToolsOrError(options: McpOptions): Promise { try { - const includedTools = await selectTools(endpoints, options); + const includedTools = selectTools(options); if (includedTools.length === 0) { console.error('No tools match the provided filters.'); process.exit(1); @@ -64,45 +59,3 @@ async function selectToolsOrError(endpoints: Endpoint[], options: McpOptions): P process.exit(1); } } - -function listAllTools() { - if (endpoints.length === 0) { - console.log('No tools available.'); - return; - } - console.log('Available tools:\n'); - - // Group endpoints by resource - const resourceGroups = new Map(); - - for (const endpoint of endpoints) { - const resource = endpoint.metadata.resource; - if (!resourceGroups.has(resource)) { - resourceGroups.set(resource, []); - } - resourceGroups.get(resource)!.push(endpoint); - } - - // Sort resources alphabetically - const sortedResources = Array.from(resourceGroups.keys()).sort(); - - // Display hierarchically by resource - for (const resource of sortedResources) { - console.log(`Resource: ${resource}`); - - const resourceEndpoints = resourceGroups.get(resource)!; - // Sort endpoints by tool name - resourceEndpoints.sort((a, b) => a.tool.name.localeCompare(b.tool.name)); - - for (const endpoint of resourceEndpoints) { - const { - tool, - metadata: { operation, tags }, - } = endpoint; - - console.log(` - ${tool.name} (${operation}) ${tags.length > 0 ? `tags: ${tags.join(', ')}` : ''}`); - console.log(` Description: ${tool.description}`); - } - console.log(''); - } -} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index b6ff5976..6c8bb8d1 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -2,140 +2,31 @@ import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import z from 'zod'; -import { endpoints, Filter } from './tools'; -import { ClientCapabilities, knownClients, ClientType } from './compat'; export type CLIOptions = McpOptions & { - list: boolean; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; }; export type McpOptions = { - client?: ClientType | undefined; - includeDynamicTools?: boolean | undefined; - includeAllTools?: boolean | undefined; - includeCodeTools?: boolean | undefined; includeDocsTools?: boolean | undefined; - filters?: Filter[] | undefined; - capabilities?: Partial | undefined; }; -const CAPABILITY_CHOICES = [ - 'top-level-unions', - 'valid-json', - 'refs', - 'unions', - 'formats', - 'tool-name-length', -] as const; - -type Capability = (typeof CAPABILITY_CHOICES)[number]; - -function parseCapabilityValue(cap: string): { name: Capability; value?: number } { - if (cap.startsWith('tool-name-length=')) { - const parts = cap.split('='); - if (parts.length === 2) { - const length = parseInt(parts[1]!, 10); - if (!isNaN(length)) { - return { name: 'tool-name-length', value: length }; - } - throw new Error(`Invalid tool-name-length value: ${parts[1]}. Expected a number.`); - } - throw new Error(`Invalid format for tool-name-length. Expected tool-name-length=N.`); - } - if (!CAPABILITY_CHOICES.includes(cap as Capability)) { - throw new Error(`Unknown capability: ${cap}. Valid capabilities are: ${CAPABILITY_CHOICES.join(', ')}`); - } - return { name: cap as Capability }; -} - export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) .option('tools', { type: 'string', array: true, - choices: ['dynamic', 'all', 'code', 'docs'], + choices: ['code', 'docs'], description: 'Use dynamic tools or all tools', }) .option('no-tools', { type: 'string', array: true, - choices: ['dynamic', 'all', 'code', 'docs'], + choices: ['code', 'docs'], description: 'Do not use any dynamic or all tools', }) - .option('tool', { - type: 'string', - array: true, - description: 'Include tools matching the specified names', - }) - .option('resource', { - type: 'string', - array: true, - description: 'Include tools matching the specified resources', - }) - .option('operation', { - type: 'string', - array: true, - choices: ['read', 'write'], - description: 'Include tools matching the specified operations', - }) - .option('tag', { - type: 'string', - array: true, - description: 'Include tools with the specified tags', - }) - .option('no-tool', { - type: 'string', - array: true, - description: 'Exclude tools matching the specified names', - }) - .option('no-resource', { - type: 'string', - array: true, - description: 'Exclude tools matching the specified resources', - }) - .option('no-operation', { - type: 'string', - array: true, - description: 'Exclude tools matching the specified operations', - }) - .option('no-tag', { - type: 'string', - array: true, - description: 'Exclude tools with the specified tags', - }) - .option('list', { - type: 'boolean', - description: 'List all tools and exit', - }) - .option('client', { - type: 'string', - choices: Object.keys(knownClients), - description: 'Specify the MCP client being used', - }) - .option('capability', { - type: 'string', - array: true, - description: 'Specify client capabilities', - coerce: (values: string[]) => { - return values.flatMap((v) => v.split(',')); - }, - }) - .option('no-capability', { - type: 'string', - array: true, - description: 'Unset client capabilities', - choices: CAPABILITY_CHOICES, - coerce: (values: string[]) => { - return values.flatMap((v) => v.split(',')); - }, - }) - .option('describe-capabilities', { - type: 'boolean', - description: 'Print detailed explanation of client capabilities and exit', - }) .option('transport', { type: 'string', choices: ['stdio', 'http'], @@ -152,122 +43,19 @@ export function parseCLIOptions(): CLIOptions { }) .help(); - for (const [command, desc] of examples()) { - opts.example(command, desc); - } - const argv = opts.parseSync(); - // Handle describe-capabilities flag - if (argv.describeCapabilities) { - console.log(getCapabilitiesExplanation()); - process.exit(0); - } - - const filters: Filter[] = []; - - // Helper function to support comma-separated values - const splitValues = (values: string[] | undefined): string[] => { - if (!values) return []; - return values.flatMap((v) => v.split(',')); - }; - - for (const tag of splitValues(argv.tag)) { - filters.push({ type: 'tag', op: 'include', value: tag }); - } - - for (const tag of splitValues(argv.noTag)) { - filters.push({ type: 'tag', op: 'exclude', value: tag }); - } - - for (const resource of splitValues(argv.resource)) { - filters.push({ type: 'resource', op: 'include', value: resource }); - } - - for (const resource of splitValues(argv.noResource)) { - filters.push({ type: 'resource', op: 'exclude', value: resource }); - } - - for (const tool of splitValues(argv.tool)) { - filters.push({ type: 'tool', op: 'include', value: tool }); - } - - for (const tool of splitValues(argv.noTool)) { - filters.push({ type: 'tool', op: 'exclude', value: tool }); - } - - for (const operation of splitValues(argv.operation)) { - filters.push({ type: 'operation', op: 'include', value: operation }); - } - - for (const operation of splitValues(argv.noOperation)) { - filters.push({ type: 'operation', op: 'exclude', value: operation }); - } - - // Parse client capabilities - const clientCapabilities: Partial = {}; - - // Apply individual capability overrides - if (Array.isArray(argv.capability)) { - for (const cap of argv.capability) { - const parsedCap = parseCapabilityValue(cap); - if (parsedCap.name === 'top-level-unions') { - clientCapabilities.topLevelUnions = true; - } else if (parsedCap.name === 'valid-json') { - clientCapabilities.validJson = true; - } else if (parsedCap.name === 'refs') { - clientCapabilities.refs = true; - } else if (parsedCap.name === 'unions') { - clientCapabilities.unions = true; - } else if (parsedCap.name === 'formats') { - clientCapabilities.formats = true; - } else if (parsedCap.name === 'tool-name-length') { - clientCapabilities.toolNameLength = parsedCap.value; - } - } - } - - // Handle no-capability options to unset capabilities - if (Array.isArray(argv.noCapability)) { - for (const cap of argv.noCapability) { - if (cap === 'top-level-unions') { - clientCapabilities.topLevelUnions = false; - } else if (cap === 'valid-json') { - clientCapabilities.validJson = false; - } else if (cap === 'refs') { - clientCapabilities.refs = false; - } else if (cap === 'unions') { - clientCapabilities.unions = false; - } else if (cap === 'formats') { - clientCapabilities.formats = false; - } else if (cap === 'tool-name-length') { - clientCapabilities.toolNameLength = undefined; - } - } - } - - const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code' | 'docs') => + const shouldIncludeToolType = (toolType: 'code' | 'docs') => argv.noTools?.includes(toolType) ? false : argv.tools?.includes(toolType) ? true : undefined; - const includeDynamicTools = shouldIncludeToolType('dynamic'); - const includeAllTools = shouldIncludeToolType('all'); - const includeCodeTools = shouldIncludeToolType('code'); const includeDocsTools = shouldIncludeToolType('docs'); const transport = argv.transport as 'stdio' | 'http'; - const client = argv.client as ClientType; return { - client: client && client !== 'infer' && knownClients[client] ? client : undefined, - includeDynamicTools, - includeAllTools, - includeCodeTools, includeDocsTools, - filters, - capabilities: clientCapabilities, - list: argv.list || false, transport, port: argv.port, socket: argv.socket, @@ -284,190 +72,21 @@ const coerceArray = (zodType: T) => ); const QueryOptions = z.object({ - tools: coerceArray(z.enum(['dynamic', 'all', 'code', 'docs'])).describe('Specify which MCP tools to use'), - no_tools: coerceArray(z.enum(['dynamic', 'all', 'code', 'docs'])).describe( - 'Specify which MCP tools to not use.', - ), + tools: coerceArray(z.enum(['code', 'docs'])).describe('Specify which MCP tools to use'), + no_tools: coerceArray(z.enum(['code', 'docs'])).describe('Specify which MCP tools to not use.'), tool: coerceArray(z.string()).describe('Include tools matching the specified names'), - resource: coerceArray(z.string()).describe('Include tools matching the specified resources'), - operation: coerceArray(z.enum(['read', 'write'])).describe( - 'Include tools matching the specified operations', - ), - tag: coerceArray(z.string()).describe('Include tools with the specified tags'), - no_tool: coerceArray(z.string()).describe('Exclude tools matching the specified names'), - no_resource: coerceArray(z.string()).describe('Exclude tools matching the specified resources'), - no_operation: coerceArray(z.enum(['read', 'write'])).describe( - 'Exclude tools matching the specified operations', - ), - no_tag: coerceArray(z.string()).describe('Exclude tools with the specified tags'), - client: ClientType.optional().describe('Specify the MCP client being used'), - capability: coerceArray(z.string()).describe('Specify client capabilities'), - no_capability: coerceArray(z.enum(CAPABILITY_CHOICES)).describe('Unset client capabilities'), }); export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): McpOptions { const queryObject = typeof query === 'string' ? qs.parse(query) : query; const queryOptions = QueryOptions.parse(queryObject); - const filters: Filter[] = [...(defaultOptions.filters ?? [])]; - - for (const resource of queryOptions.resource || []) { - filters.push({ type: 'resource', op: 'include', value: resource }); - } - for (const operation of queryOptions.operation || []) { - filters.push({ type: 'operation', op: 'include', value: operation }); - } - for (const tag of queryOptions.tag || []) { - filters.push({ type: 'tag', op: 'include', value: tag }); - } - for (const tool of queryOptions.tool || []) { - filters.push({ type: 'tool', op: 'include', value: tool }); - } - for (const resource of queryOptions.no_resource || []) { - filters.push({ type: 'resource', op: 'exclude', value: resource }); - } - for (const operation of queryOptions.no_operation || []) { - filters.push({ type: 'operation', op: 'exclude', value: operation }); - } - for (const tag of queryOptions.no_tag || []) { - filters.push({ type: 'tag', op: 'exclude', value: tag }); - } - for (const tool of queryOptions.no_tool || []) { - filters.push({ type: 'tool', op: 'exclude', value: tool }); - } - - // Parse client capabilities - const clientCapabilities: Partial = { ...defaultOptions.capabilities }; - - for (const cap of queryOptions.capability || []) { - const parsed = parseCapabilityValue(cap); - if (parsed.name === 'top-level-unions') { - clientCapabilities.topLevelUnions = true; - } else if (parsed.name === 'valid-json') { - clientCapabilities.validJson = true; - } else if (parsed.name === 'refs') { - clientCapabilities.refs = true; - } else if (parsed.name === 'unions') { - clientCapabilities.unions = true; - } else if (parsed.name === 'formats') { - clientCapabilities.formats = true; - } else if (parsed.name === 'tool-name-length') { - clientCapabilities.toolNameLength = parsed.value; - } - } - - for (const cap of queryOptions.no_capability || []) { - if (cap === 'top-level-unions') { - clientCapabilities.topLevelUnions = false; - } else if (cap === 'valid-json') { - clientCapabilities.validJson = false; - } else if (cap === 'refs') { - clientCapabilities.refs = false; - } else if (cap === 'unions') { - clientCapabilities.unions = false; - } else if (cap === 'formats') { - clientCapabilities.formats = false; - } else if (cap === 'tool-name-length') { - clientCapabilities.toolNameLength = undefined; - } - } - - let dynamicTools: boolean | undefined = - queryOptions.no_tools && queryOptions.no_tools?.includes('dynamic') ? false - : queryOptions.tools?.includes('dynamic') ? true - : defaultOptions.includeDynamicTools; - - let allTools: boolean | undefined = - queryOptions.no_tools && queryOptions.no_tools?.includes('all') ? false - : queryOptions.tools?.includes('all') ? true - : defaultOptions.includeAllTools; - let docsTools: boolean | undefined = queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false : queryOptions.tools?.includes('docs') ? true : defaultOptions.includeDocsTools; - let codeTools: boolean | undefined = - queryOptions.no_tools && queryOptions.no_tools?.includes('code') ? false - : queryOptions.tools?.includes('code') && defaultOptions.includeCodeTools ? true - : defaultOptions.includeCodeTools; - return { - client: queryOptions.client ?? defaultOptions.client, - includeDynamicTools: dynamicTools, - includeAllTools: allTools, - includeCodeTools: codeTools, includeDocsTools: docsTools, - filters, - capabilities: clientCapabilities, }; } - -function getCapabilitiesExplanation(): string { - return ` -Client Capabilities Explanation: - -Different Language Models (LLMs) and the MCP clients that use them have varying limitations in how they handle tool schemas. Capability flags allow you to inform the MCP server about these limitations. - -When a capability flag is set to false, the MCP server will automatically adjust the tool schemas to work around that limitation, ensuring broader compatibility. - -Available Capabilities: - -# top-level-unions -Some clients/LLMs do not support JSON schemas with a union type (anyOf) at the root level. If a client lacks this capability, the MCP server splits tools with top-level unions into multiple separate tools, one for each variant in the union. - -# refs -Some clients/LLMs do not support $ref pointers for schema reuse. If a client lacks this capability, the MCP server automatically inlines all references ($defs) directly into the schema. Properties that would cause circular references are removed during this process. - -# valid-json -Some clients/LLMs may incorrectly send arguments as a JSON-encoded string instead of a proper JSON object. If a client *has* this capability, the MCP server will attempt to parse string values as JSON if the initial validation against the schema fails. - -# unions -Some clients/LLMs do not support union types (anyOf) in JSON schemas. If a client lacks this capability, the MCP server removes all anyOf fields and uses only the first variant as the schema. - -# formats -Some clients/LLMs do not support the 'format' keyword in JSON Schema specifications. If a client lacks this capability, the MCP server removes all format fields and appends the format information to the field's description in parentheses. - -# tool-name-length=N -Some clients/LLMs impose a maximum length on tool names. If this capability is set, the MCP server will automatically truncate tool names exceeding the specified length (N), ensuring uniqueness by appending numbers if necessary. - -Client Presets (--client): -Presets like '--client=openai-agents' or '--client=cursor' automatically configure these capabilities based on current known limitations of those clients, simplifying setup. - -Current presets: -${JSON.stringify(knownClients, null, 2)} - `; -} - -function examples(): [string, string][] { - const firstEndpoint = endpoints[0]!; - const secondEndpoint = - endpoints.find((e) => e.metadata.resource !== firstEndpoint.metadata.resource) || endpoints[1]; - const tag = endpoints.find((e) => e.metadata.tags.length > 0)?.metadata.tags[0]; - const otherEndpoint = secondEndpoint || firstEndpoint; - - return [ - [ - `--tool="${firstEndpoint.tool.name}" ${secondEndpoint ? `--tool="${secondEndpoint.tool.name}"` : ''}`, - 'Include tools by name', - ], - [ - `--resource="${firstEndpoint.metadata.resource}" --operation="read"`, - 'Filter by resource and operation', - ], - [ - `--resource="${otherEndpoint.metadata.resource}*" --no-tool="${otherEndpoint.tool.name}"`, - 'Use resource wildcards and exclusions', - ], - [`--client="cursor"`, 'Adjust schemas to be more compatible with Cursor'], - [ - `--capability="top-level-unions" --capability="tool-name-length=40"`, - 'Specify individual client capabilities', - ], - [ - `--client="cursor" --no-capability="tool-name-length"`, - 'Use cursor client preset but remove tool name length limit', - ], - ...(tag ? [[`--tag="${tag}"`, 'Filter based on tags'] as [string, string]] : []), - ]; -} diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index e2d82a3a..8bb289ab 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -2,39 +2,26 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Endpoint, endpoints, HandlerFunction, query } from './tools'; import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, - Implementation, - Tool, } from '@modelcontextprotocol/sdk/types.js'; import { ClientOptions } from '@stainless-api/sdk'; import Stainless from '@stainless-api/sdk'; -import { - applyCompatibilityTransformations, - ClientCapabilities, - defaultClientCapabilities, - knownClients, - parseEmbeddedJSON, -} from './compat'; -import { dynamicTools } from './dynamic-tools'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; +import { HandlerFunction, McpTool } from './types'; export { McpOptions } from './options'; -export { ClientType } from './compat'; -export { Filter } from './tools'; export { ClientOptions } from '@stainless-api/sdk'; -export { endpoints } from './tools'; export const newMcpServer = () => new McpServer( { name: 'stainless_api_sdk_api', - version: '0.1.0-alpha.19', + version: '0.1.0-alpha.20', }, { capabilities: { tools: {}, logging: {} } }, ); @@ -52,25 +39,6 @@ export function initMcpServer(params: { mcpOptions?: McpOptions; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; - const mcpOptions = params.mcpOptions ?? {}; - - let providedEndpoints: Endpoint[] | null = null; - let endpointMap: Record | null = null; - - const initTools = async (implementation?: Implementation) => { - if (implementation && (!mcpOptions.client || mcpOptions.client === 'infer')) { - mcpOptions.client = - implementation.name.toLowerCase().includes('claude') ? 'claude' - : implementation.name.toLowerCase().includes('cursor') ? 'cursor' - : undefined; - mcpOptions.capabilities = { - ...(mcpOptions.client && knownClients[mcpOptions.client]), - ...mcpOptions.capabilities, - }; - } - providedEndpoints ??= await selectTools(endpoints, mcpOptions); - endpointMap ??= Object.fromEntries(providedEndpoints.map((endpoint) => [endpoint.tool.name, endpoint])); - }; const logAtLevel = (level: 'debug' | 'info' | 'warning' | 'error') => @@ -100,26 +68,23 @@ export function initMcpServer(params: { }, }); + const providedTools = selectTools(params.mcpOptions); + const toolMap = Object.fromEntries(providedTools.map((mcpTool) => [mcpTool.tool.name, mcpTool])); + server.setRequestHandler(ListToolsRequestSchema, async () => { - if (providedEndpoints === null) { - await initTools(server.getClientVersion()); - } return { - tools: providedEndpoints!.map((endpoint) => endpoint.tool), + tools: providedTools.map((mcpTool) => mcpTool.tool), }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { - if (endpointMap === null) { - await initTools(server.getClientVersion()); - } const { name, arguments: args } = request.params; - const endpoint = endpointMap![name]; - if (!endpoint) { + const mcpTool = toolMap[name]; + if (!mcpTool) { throw new Error(`Unknown tool: ${name}`); } - return executeHandler(endpoint.tool, endpoint.handler, client, args, mcpOptions.capabilities); + return executeHandler(mcpTool.handler, client, args); }); server.setRequestHandler(SetLevelRequestSchema, async (request) => { @@ -149,47 +114,22 @@ export function initMcpServer(params: { /** * Selects the tools to include in the MCP Server based on the provided options. */ -export async function selectTools(endpoints: Endpoint[], options?: McpOptions): Promise { - const filteredEndpoints = query(options?.filters ?? [], endpoints); - - let includedTools = filteredEndpoints.slice(); - - if (includedTools.length > 0) { - if (options?.includeDynamicTools) { - includedTools = dynamicTools(includedTools); - } - } else { - if (options?.includeAllTools) { - includedTools = endpoints.slice(); - } else if (options?.includeDynamicTools) { - includedTools = dynamicTools(endpoints); - } else if (options?.includeCodeTools) { - includedTools = [await codeTool()]; - } else { - includedTools = endpoints.slice(); - } - } +export function selectTools(options?: McpOptions): McpTool[] { + const includedTools = [codeTool()]; if (options?.includeDocsTools ?? true) { includedTools.push(docsSearchTool); } - const capabilities = { ...defaultClientCapabilities, ...options?.capabilities }; - return applyCompatibilityTransformations(includedTools, capabilities); + return includedTools; } /** * Runs the provided handler with the given client and arguments. */ export async function executeHandler( - tool: Tool, handler: HandlerFunction, client: Stainless, args: Record | undefined, - compatibilityOptions?: Partial, ) { - const options = { ...defaultClientCapabilities, ...compatibilityOptions }; - if (!options.validJson && args) { - args = parseEmbeddedJSON(args, tool.inputSchema); - } return await handler(client, args || {}); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index d902a5bb..f07696f3 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,11 +1,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { initMcpServer, newMcpServer } from './server'; -import { McpOptions } from './options'; -export const launchStdioServer = async (options: McpOptions) => { +export const launchStdioServer = async () => { const server = newMcpServer(); - initMcpServer({ server, mcpOptions: options }); + initMcpServer({ server }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/src/tools.ts b/packages/mcp-server/src/tools.ts deleted file mode 100644 index 7e516de7..00000000 --- a/packages/mcp-server/src/tools.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tools/index'; diff --git a/packages/mcp-server/src/tools/builds/create-builds.ts b/packages/mcp-server/src/tools/builds/create-builds.ts deleted file mode 100644 index 77778aad..00000000 --- a/packages/mcp-server/src/tools/builds/create-builds.ts +++ /dev/null @@ -1,139 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'builds', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v0/builds', -}; - -export const tool: Tool = { - name: 'create_builds', - description: - 'Create a build, on top of a project branch, against a given input revision.\n\nThe project branch will be modified so that its latest set of config files\npoints to the one specified by the input revision.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - description: 'Project name', - }, - revision: { - anyOf: [ - { - type: 'string', - description: 'A branch name, commit SHA, or merge command in the format "base..head"', - }, - { - type: 'object', - description: 'File contents to commit directly', - additionalProperties: true, - }, - ], - description: - 'Specifies what to build: a branch name, commit SHA, merge command\n("base..head"), or file contents.', - }, - allow_empty: { - type: 'boolean', - description: 'Whether to allow empty commits (no changes). Defaults to false.', - }, - branch: { - type: 'string', - description: - 'The project branch to use for the build. If not specified, the\nbranch is inferred from the `revision`, and will 400 when that\nis not possible.', - }, - commit_message: { - type: 'string', - description: 'Optional commit message to use when creating a new commit.', - }, - target_commit_messages: { - type: 'object', - description: - 'Optional commit messages to use for each SDK when making a new commit.\nSDKs not represented in this object will fallback to the optional\n`commit_message` parameter, or will fallback further to the default\ncommit message.', - properties: { - cli: { - type: 'string', - }, - csharp: { - type: 'string', - }, - go: { - type: 'string', - }, - java: { - type: 'string', - }, - kotlin: { - type: 'string', - }, - node: { - type: 'string', - }, - php: { - type: 'string', - }, - python: { - type: 'string', - }, - ruby: { - type: 'string', - }, - terraform: { - type: 'string', - }, - typescript: { - type: 'string', - }, - }, - }, - targets: { - type: 'array', - description: - 'Optional list of SDK targets to build. If not specified, all configured\ntargets will be built.', - items: { - $ref: '#/$defs/target', - }, - }, - }, - required: ['project', 'revision'], - $defs: { - target: { - type: 'string', - enum: [ - 'node', - 'typescript', - 'python', - 'go', - 'java', - 'kotlin', - 'ruby', - 'terraform', - 'cli', - 'php', - 'csharp', - ], - }, - }, - }, - annotations: {}, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const body = args as any; - try { - return asTextContentResult(await client.builds.create(body)); - } catch (error) { - if (error instanceof Stainless.APIError) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/builds/diagnostics/list-builds-diagnostics.ts b/packages/mcp-server/src/tools/builds/diagnostics/list-builds-diagnostics.ts deleted file mode 100644 index 594b7914..00000000 --- a/packages/mcp-server/src/tools/builds/diagnostics/list-builds-diagnostics.ts +++ /dev/null @@ -1,72 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'builds.diagnostics', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/builds/{buildId}/diagnostics', -}; - -export const tool: Tool = { - name: 'list_builds_diagnostics', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nGet the list of diagnostics for a given build.\n\nIf no language targets are specified, diagnostics for all languages are returned.\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n data: {\n type: 'array',\n items: {\n $ref: '#/$defs/build_diagnostic'\n }\n },\n has_more: {\n type: 'boolean'\n },\n next_cursor: {\n type: 'string'\n }\n },\n required: [ 'data',\n 'has_more'\n ],\n $defs: {\n build_diagnostic: {\n type: 'object',\n properties: {\n code: {\n type: 'string',\n description: 'The kind of diagnostic.'\n },\n ignored: {\n type: 'boolean',\n description: 'Whether the diagnostic is ignored in the Stainless config.'\n },\n level: {\n type: 'string',\n description: 'The severity of the diagnostic.',\n enum: [ 'fatal',\n 'error',\n 'warning',\n 'note'\n ]\n },\n message: {\n type: 'string',\n description: 'A description of the diagnostic.'\n },\n more: {\n $ref: '#/$defs/build_diagnostic_more'\n },\n config_ref: {\n type: 'string',\n description: 'A JSON pointer to a relevant field in the Stainless config.'\n },\n oas_ref: {\n type: 'string',\n description: 'A JSON pointer to a relevant field in the OpenAPI spec.'\n }\n },\n required: [ 'code',\n 'ignored',\n 'level',\n 'message',\n 'more'\n ]\n },\n build_diagnostic_more: {\n anyOf: [ {\n type: 'object',\n properties: {\n markdown: {\n type: 'string'\n },\n type: {\n type: 'string',\n enum: [ 'markdown'\n ]\n }\n },\n required: [ 'markdown',\n 'type'\n ]\n },\n {\n type: 'object',\n properties: {\n raw: {\n type: 'string'\n },\n type: {\n type: 'string',\n enum: [ 'raw'\n ]\n }\n },\n required: [ 'raw',\n 'type'\n ]\n }\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - buildId: { - type: 'string', - description: 'Build ID', - }, - cursor: { - type: 'string', - description: 'Pagination cursor from a previous response', - }, - limit: { - type: 'number', - description: 'Maximum number of diagnostics to return, defaults to 100 (maximum: 100)', - }, - severity: { - type: 'string', - description: 'Includes the given severity and above (fatal > error > warning > note).', - enum: ['fatal', 'error', 'warning', 'note'], - }, - targets: { - type: 'string', - description: 'Optional comma-delimited list of language targets to filter diagnostics by', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['buildId'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { buildId, jq_filter, ...body } = args as any; - const response = await client.builds.diagnostics.list(buildId, body).asResponse(); - try { - return asTextContentResult(await maybeFilter(jq_filter, await response.json())); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/builds/list-builds.ts b/packages/mcp-server/src/tools/builds/list-builds.ts deleted file mode 100644 index c8a7a9a1..00000000 --- a/packages/mcp-server/src/tools/builds/list-builds.ts +++ /dev/null @@ -1,74 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'builds', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/builds', -}; - -export const tool: Tool = { - name: 'list_builds', - description: - 'List user-triggered builds for a given project.\n\nAn optional revision can be specified to filter by config commit SHA, or\nhashes of file contents.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - description: 'Project name', - }, - branch: { - type: 'string', - description: 'Branch name', - }, - cursor: { - type: 'string', - description: 'Pagination cursor from a previous response.', - }, - limit: { - type: 'number', - description: 'Maximum number of builds to return, defaults to 10 (maximum: 100).', - }, - revision: { - anyOf: [ - { - type: 'string', - description: 'A config commit SHA used for the build', - }, - { - type: 'object', - description: 'Hash of the files used for the build', - additionalProperties: true, - }, - ], - description: 'A config commit SHA used for the build', - }, - }, - required: ['project'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const body = args as any; - const response = await client.builds.list(body).asResponse(); - try { - return asTextContentResult(await response.json()); - } catch (error) { - if (error instanceof Stainless.APIError) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/builds/retrieve-builds.ts b/packages/mcp-server/src/tools/builds/retrieve-builds.ts deleted file mode 100644 index 6ad06c9d..00000000 --- a/packages/mcp-server/src/tools/builds/retrieve-builds.ts +++ /dev/null @@ -1,54 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'builds', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/builds/{buildId}', -}; - -export const tool: Tool = { - name: 'retrieve_builds', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nRetrieve a build by its ID.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/build',\n $defs: {\n build: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Build ID'\n },\n config_commit: {\n type: 'string'\n },\n created_at: {\n type: 'string',\n format: 'date-time'\n },\n documented_spec: {\n anyOf: [ {\n type: 'object',\n properties: {\n content: {\n type: 'string'\n },\n type: {\n type: 'string',\n enum: [ 'content'\n ]\n }\n },\n required: [ 'content',\n 'type'\n ]\n },\n {\n type: 'object',\n properties: {\n expires: {\n type: 'string',\n format: 'date-time'\n },\n type: {\n type: 'string',\n enum: [ 'url'\n ]\n },\n url: {\n type: 'string'\n }\n },\n required: [ 'expires',\n 'type',\n 'url'\n ]\n }\n ]\n },\n object: {\n type: 'string',\n enum: [ 'build'\n ]\n },\n org: {\n type: 'string'\n },\n project: {\n type: 'string'\n },\n targets: {\n type: 'object',\n properties: {\n cli: {\n $ref: '#/$defs/build_target'\n },\n csharp: {\n $ref: '#/$defs/build_target'\n },\n go: {\n $ref: '#/$defs/build_target'\n },\n java: {\n $ref: '#/$defs/build_target'\n },\n kotlin: {\n $ref: '#/$defs/build_target'\n },\n node: {\n $ref: '#/$defs/build_target'\n },\n php: {\n $ref: '#/$defs/build_target'\n },\n python: {\n $ref: '#/$defs/build_target'\n },\n ruby: {\n $ref: '#/$defs/build_target'\n },\n terraform: {\n $ref: '#/$defs/build_target'\n },\n typescript: {\n $ref: '#/$defs/build_target'\n }\n }\n },\n updated_at: {\n type: 'string',\n format: 'date-time'\n }\n },\n required: [ 'id',\n 'config_commit',\n 'created_at',\n 'documented_spec',\n 'object',\n 'org',\n 'project',\n 'targets',\n 'updated_at'\n ]\n },\n build_target: {\n type: 'object',\n properties: {\n commit: {\n anyOf: [ {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: [ 'not_started'\n ]\n }\n },\n required: [ 'status'\n ]\n },\n {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: [ 'queued'\n ]\n }\n },\n required: [ 'status'\n ]\n },\n {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: [ 'in_progress'\n ]\n }\n },\n required: [ 'status'\n ]\n },\n {\n type: 'object',\n properties: {\n completed: {\n type: 'object',\n properties: {\n commit: {\n $ref: '#/$defs/commit'\n },\n conclusion: {\n type: 'string',\n enum: [ 'error',\n 'warning',\n 'note',\n 'success',\n 'merge_conflict',\n 'upstream_merge_conflict',\n 'fatal',\n 'payment_required',\n 'cancelled',\n 'timed_out',\n 'noop',\n 'version_bump'\n ]\n },\n merge_conflict_pr: {\n type: 'object',\n properties: {\n number: {\n type: 'number'\n },\n repo: {\n type: 'object',\n properties: {\n name: {\n type: 'string'\n },\n owner: {\n type: 'string'\n }\n },\n required: [ 'name',\n 'owner'\n ]\n }\n },\n required: [ 'number',\n 'repo'\n ]\n }\n },\n required: [ 'commit',\n 'conclusion',\n 'merge_conflict_pr'\n ]\n },\n status: {\n type: 'string',\n enum: [ 'completed'\n ]\n }\n },\n required: [ 'completed',\n 'status'\n ]\n }\n ]\n },\n install_url: {\n type: 'string'\n },\n lint: {\n $ref: '#/$defs/check_step'\n },\n object: {\n type: 'string',\n enum: [ 'build_target'\n ]\n },\n status: {\n type: 'string',\n enum: [ 'not_started',\n 'codegen',\n 'postgen',\n 'completed'\n ]\n },\n test: {\n $ref: '#/$defs/check_step'\n },\n build: {\n $ref: '#/$defs/check_step'\n }\n },\n required: [ 'commit',\n 'install_url',\n 'lint',\n 'object',\n 'status',\n 'test'\n ]\n },\n commit: {\n type: 'object',\n properties: {\n repo: {\n type: 'object',\n properties: {\n branch: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n owner: {\n type: 'string'\n }\n },\n required: [ 'branch',\n 'name',\n 'owner'\n ]\n },\n sha: {\n type: 'string'\n }\n },\n required: [ 'repo',\n 'sha'\n ]\n },\n check_step: {\n anyOf: [ {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: [ 'not_started'\n ]\n }\n },\n required: [ 'status'\n ]\n },\n {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: [ 'queued'\n ]\n }\n },\n required: [ 'status'\n ]\n },\n {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: [ 'in_progress'\n ]\n }\n },\n required: [ 'status'\n ]\n },\n {\n type: 'object',\n properties: {\n completed: {\n type: 'object',\n properties: {\n conclusion: {\n type: 'string',\n enum: [ 'success',\n 'failure',\n 'skipped',\n 'cancelled',\n 'action_required',\n 'neutral',\n 'timed_out'\n ]\n },\n url: {\n type: 'string'\n }\n },\n required: [ 'conclusion',\n 'url'\n ]\n },\n status: {\n type: 'string',\n enum: [ 'completed'\n ]\n }\n },\n required: [ 'completed',\n 'status'\n ]\n }\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - buildId: { - type: 'string', - description: 'Build ID', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['buildId'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { buildId, jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.builds.retrieve(buildId))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/builds/target-outputs/retrieve-builds-target-outputs.ts b/packages/mcp-server/src/tools/builds/target-outputs/retrieve-builds-target-outputs.ts deleted file mode 100644 index 44732ce0..00000000 --- a/packages/mcp-server/src/tools/builds/target-outputs/retrieve-builds-target-outputs.ts +++ /dev/null @@ -1,82 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'builds.target_outputs', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/build_target_outputs', -}; - -export const tool: Tool = { - name: 'retrieve_builds_target_outputs', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nRetrieve a method to download an output for a given build target.\n\nIf the requested type of output is `source`, and the requested output\nmethod is `url`, a download link to a tarball of the source files is\nreturned. If the requested output method is `git`, a Git remote, ref,\nand access token (if necessary) is returned.\n\nOtherwise, the possible types of outputs are specific to the requested\ntarget, and the output method _must_ be `url`. See the documentation for\n`type` for more information.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/target_output_retrieve_response',\n $defs: {\n target_output_retrieve_response: {\n anyOf: [ {\n type: 'object',\n properties: {\n output: {\n type: 'string',\n enum: [ 'url'\n ]\n },\n target: {\n $ref: '#/$defs/target'\n },\n type: {\n type: 'string',\n enum: [ 'source',\n 'dist',\n 'wheel'\n ]\n },\n url: {\n type: 'string',\n description: 'URL for direct download'\n }\n },\n required: [ 'output',\n 'target',\n 'type',\n 'url'\n ]\n },\n {\n type: 'object',\n properties: {\n token: {\n type: 'string',\n description: 'Temporary GitHub access token'\n },\n output: {\n type: 'string',\n enum: [ 'git'\n ]\n },\n ref: {\n type: 'string',\n description: 'Git reference (commit SHA, branch, or tag)'\n },\n target: {\n $ref: '#/$defs/target'\n },\n type: {\n type: 'string',\n enum: [ 'source',\n 'dist',\n 'wheel'\n ]\n },\n url: {\n type: 'string',\n description: 'URL to git remote'\n }\n },\n required: [ 'token',\n 'output',\n 'ref',\n 'target',\n 'type',\n 'url'\n ]\n }\n ]\n },\n target: {\n type: 'string',\n enum: [ 'node',\n 'typescript',\n 'python',\n 'go',\n 'java',\n 'kotlin',\n 'ruby',\n 'terraform',\n 'cli',\n 'php',\n 'csharp'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - build_id: { - type: 'string', - description: 'Build ID', - }, - target: { - type: 'string', - description: 'SDK language target name', - enum: [ - 'node', - 'typescript', - 'python', - 'go', - 'java', - 'kotlin', - 'ruby', - 'terraform', - 'cli', - 'php', - 'csharp', - ], - }, - type: { - type: 'string', - enum: ['source', 'dist', 'wheel'], - }, - output: { - type: 'string', - description: 'Output format: url (download URL) or git (temporary access token).', - enum: ['url', 'git'], - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['build_id', 'target', 'type'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - try { - return asTextContentResult( - await maybeFilter(jq_filter, await client.builds.targetOutputs.retrieve(body)), - ); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts deleted file mode 100644 index 3402cc83..00000000 --- a/packages/mcp-server/src/tools/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, Endpoint, HandlerFunction } from './types'; - -export { Metadata, Endpoint, HandlerFunction }; - -import create_projects from './projects/create-projects'; -import retrieve_projects from './projects/retrieve-projects'; -import update_projects from './projects/update-projects'; -import list_projects from './projects/list-projects'; -import create_projects_branches from './projects/branches/create-projects-branches'; -import retrieve_projects_branches from './projects/branches/retrieve-projects-branches'; -import list_projects_branches from './projects/branches/list-projects-branches'; -import delete_projects_branches from './projects/branches/delete-projects-branches'; -import rebase_projects_branches from './projects/branches/rebase-projects-branches'; -import reset_projects_branches from './projects/branches/reset-projects-branches'; -import retrieve_projects_configs from './projects/configs/retrieve-projects-configs'; -import guess_projects_configs from './projects/configs/guess-projects-configs'; -import create_builds from './builds/create-builds'; -import retrieve_builds from './builds/retrieve-builds'; -import list_builds from './builds/list-builds'; -import list_builds_diagnostics from './builds/diagnostics/list-builds-diagnostics'; -import retrieve_builds_target_outputs from './builds/target-outputs/retrieve-builds-target-outputs'; -import retrieve_orgs from './orgs/retrieve-orgs'; -import list_orgs from './orgs/list-orgs'; - -export const endpoints: Endpoint[] = []; - -function addEndpoint(endpoint: Endpoint) { - endpoints.push(endpoint); -} - -addEndpoint(create_projects); -addEndpoint(retrieve_projects); -addEndpoint(update_projects); -addEndpoint(list_projects); -addEndpoint(create_projects_branches); -addEndpoint(retrieve_projects_branches); -addEndpoint(list_projects_branches); -addEndpoint(delete_projects_branches); -addEndpoint(rebase_projects_branches); -addEndpoint(reset_projects_branches); -addEndpoint(retrieve_projects_configs); -addEndpoint(guess_projects_configs); -addEndpoint(create_builds); -addEndpoint(retrieve_builds); -addEndpoint(list_builds); -addEndpoint(list_builds_diagnostics); -addEndpoint(retrieve_builds_target_outputs); -addEndpoint(retrieve_orgs); -addEndpoint(list_orgs); - -export type Filter = { - type: 'resource' | 'operation' | 'tag' | 'tool'; - op: 'include' | 'exclude'; - value: string; -}; - -export function query(filters: Filter[], endpoints: Endpoint[]): Endpoint[] { - const allExcludes = filters.length > 0 && filters.every((filter) => filter.op === 'exclude'); - const unmatchedFilters = new Set(filters); - - const filtered = endpoints.filter((endpoint: Endpoint) => { - let included = false || allExcludes; - - for (const filter of filters) { - if (match(filter, endpoint)) { - unmatchedFilters.delete(filter); - included = filter.op === 'include'; - } - } - - return included; - }); - - // Check if any filters didn't match - const unmatched = Array.from(unmatchedFilters).filter((f) => f.type === 'tool' || f.type === 'resource'); - if (unmatched.length > 0) { - throw new Error( - `The following filters did not match any endpoints: ${unmatched - .map((f) => `${f.type}=${f.value}`) - .join(', ')}`, - ); - } - - return filtered; -} - -function match({ type, value }: Filter, endpoint: Endpoint): boolean { - switch (type) { - case 'resource': { - const regexStr = '^' + normalizeResource(value).replace(/\*/g, '.*') + '$'; - const regex = new RegExp(regexStr); - return regex.test(normalizeResource(endpoint.metadata.resource)); - } - case 'operation': - return endpoint.metadata.operation === value; - case 'tag': - return endpoint.metadata.tags.includes(value); - case 'tool': - return endpoint.tool.name === value; - } -} - -function normalizeResource(resource: string): string { - return resource.toLowerCase().replace(/[^a-z.*\-_]*/g, ''); -} diff --git a/packages/mcp-server/src/tools/orgs/list-orgs.ts b/packages/mcp-server/src/tools/orgs/list-orgs.ts deleted file mode 100644 index bc6bd4f0..00000000 --- a/packages/mcp-server/src/tools/orgs/list-orgs.ts +++ /dev/null @@ -1,50 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'orgs', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/orgs', -}; - -export const tool: Tool = { - name: 'list_orgs', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nList organizations accessible to the current authentication method.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/org_list_response',\n $defs: {\n org_list_response: {\n type: 'object',\n properties: {\n data: {\n type: 'array',\n items: {\n $ref: '#/$defs/org'\n }\n },\n has_more: {\n type: 'boolean'\n },\n next_cursor: {\n type: 'string'\n }\n },\n required: [ 'data',\n 'has_more'\n ]\n },\n org: {\n type: 'object',\n properties: {\n display_name: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'org'\n ]\n },\n slug: {\n type: 'string'\n }\n },\n required: [ 'display_name',\n 'object',\n 'slug'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.orgs.list())); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/orgs/retrieve-orgs.ts b/packages/mcp-server/src/tools/orgs/retrieve-orgs.ts deleted file mode 100644 index a70b6588..00000000 --- a/packages/mcp-server/src/tools/orgs/retrieve-orgs.ts +++ /dev/null @@ -1,53 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'orgs', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/orgs/{org}', -}; - -export const tool: Tool = { - name: 'retrieve_orgs', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nRetrieve an organization by name.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/org',\n $defs: {\n org: {\n type: 'object',\n properties: {\n display_name: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'org'\n ]\n },\n slug: {\n type: 'string'\n }\n },\n required: [ 'display_name',\n 'object',\n 'slug'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - org: { - type: 'string', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['org'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { org, jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.orgs.retrieve(org))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/branches/create-projects-branches.ts b/packages/mcp-server/src/tools/projects/branches/create-projects-branches.ts deleted file mode 100644 index 55cfd7dc..00000000 --- a/packages/mcp-server/src/tools/projects/branches/create-projects-branches.ts +++ /dev/null @@ -1,56 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.branches', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v0/projects/{project}/branches', -}; - -export const tool: Tool = { - name: 'create_projects_branches', - description: - 'Create a new branch for a project.\n\nThe branch inherits the config files from the revision pointed to by the\n`branch_from` parameter. In addition, if the revision is a branch name,\nthe branch will also inherit custom code changes from that branch.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - branch: { - type: 'string', - description: 'Branch name', - }, - branch_from: { - type: 'string', - description: 'Branch or commit SHA to branch from', - }, - force: { - type: 'boolean', - description: 'Whether to throw an error if the branch already exists. Defaults to false.', - }, - }, - required: ['project', 'branch', 'branch_from'], - }, - annotations: {}, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const body = args as any; - try { - return asTextContentResult(await client.projects.branches.create(body)); - } catch (error) { - if (error instanceof Stainless.APIError) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/branches/delete-projects-branches.ts b/packages/mcp-server/src/tools/projects/branches/delete-projects-branches.ts deleted file mode 100644 index a9d78312..00000000 --- a/packages/mcp-server/src/tools/projects/branches/delete-projects-branches.ts +++ /dev/null @@ -1,41 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.branches', - operation: 'write', - tags: [], - httpMethod: 'delete', - httpPath: '/v0/projects/{project}/branches/{branch}', -}; - -export const tool: Tool = { - name: 'delete_projects_branches', - description: 'Delete a project branch by name.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - branch: { - type: 'string', - }, - }, - required: ['project', 'branch'], - }, - annotations: { - idempotentHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { branch, ...body } = args as any; - return asTextContentResult((await client.projects.branches.delete(branch, body)) as object); -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/branches/list-projects-branches.ts b/packages/mcp-server/src/tools/projects/branches/list-projects-branches.ts deleted file mode 100644 index a6e0d433..00000000 --- a/packages/mcp-server/src/tools/projects/branches/list-projects-branches.ts +++ /dev/null @@ -1,62 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.branches', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/projects/{project}/branches', -}; - -export const tool: Tool = { - name: 'list_projects_branches', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nRetrieve a project branch by name.\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n data: {\n type: 'array',\n items: {\n $ref: '#/$defs/branch_list_response'\n }\n },\n has_more: {\n type: 'boolean'\n },\n next_cursor: {\n type: 'string'\n }\n },\n required: [ 'data',\n 'has_more'\n ],\n $defs: {\n branch_list_response: {\n type: 'object',\n description: 'A project branch names a line of development for a project. Like a Git\\nbranch, it points to a Git commit with a set of config files. In addition, a\\nproject branch also points to a set of custom code changes, corresponding to\\nGit branches in the staging repos.',\n properties: {\n branch: {\n type: 'string',\n description: 'Branch name'\n },\n config_commit: {\n type: 'object',\n description: 'A Git commit that points to the latest set of config files on a given\\nbranch.',\n properties: {\n repo: {\n type: 'object',\n properties: {\n branch: {\n type: 'string'\n },\n name: {\n type: 'string'\n },\n owner: {\n type: 'string'\n }\n },\n required: [ 'branch',\n 'name',\n 'owner'\n ]\n },\n sha: {\n type: 'string'\n }\n },\n required: [ 'repo',\n 'sha'\n ]\n },\n latest_build_id: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'project_branch'\n ]\n },\n org: {\n type: 'string'\n },\n project: {\n type: 'string',\n description: 'Project name'\n }\n },\n required: [ 'branch',\n 'config_commit',\n 'latest_build_id',\n 'object',\n 'org',\n 'project'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - cursor: { - type: 'string', - description: 'Pagination cursor from a previous response', - }, - limit: { - type: 'number', - description: 'Maximum number of items to return, defaults to 10 (maximum: 100).', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['project'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - const response = await client.projects.branches.list(body).asResponse(); - try { - return asTextContentResult(await maybeFilter(jq_filter, await response.json())); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/branches/rebase-projects-branches.ts b/packages/mcp-server/src/tools/projects/branches/rebase-projects-branches.ts deleted file mode 100644 index 8474a891..00000000 --- a/packages/mcp-server/src/tools/projects/branches/rebase-projects-branches.ts +++ /dev/null @@ -1,53 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.branches', - operation: 'write', - tags: [], - httpMethod: 'put', - httpPath: '/v0/projects/{project}/branches/{branch}/rebase', -}; - -export const tool: Tool = { - name: 'rebase_projects_branches', - description: - 'Rebase a project branch.\n\nThe branch is rebased onto the `base` branch or commit SHA, inheriting\nany config and custom code changes.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - branch: { - type: 'string', - }, - base: { - type: 'string', - description: 'The branch or commit SHA to rebase onto. Defaults to "main".', - }, - }, - required: ['project', 'branch'], - }, - annotations: { - idempotentHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { branch, ...body } = args as any; - try { - return asTextContentResult(await client.projects.branches.rebase(branch, body)); - } catch (error) { - if (error instanceof Stainless.APIError) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/branches/reset-projects-branches.ts b/packages/mcp-server/src/tools/projects/branches/reset-projects-branches.ts deleted file mode 100644 index 0e1f1d31..00000000 --- a/packages/mcp-server/src/tools/projects/branches/reset-projects-branches.ts +++ /dev/null @@ -1,54 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.branches', - operation: 'write', - tags: [], - httpMethod: 'put', - httpPath: '/v0/projects/{project}/branches/{branch}/reset', -}; - -export const tool: Tool = { - name: 'reset_projects_branches', - description: - 'Reset a project branch.\n\nIf `branch` === `main`, the branch is reset to `target_config_sha`. Otherwise, the\nbranch is reset to `main`.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - branch: { - type: 'string', - }, - target_config_sha: { - type: 'string', - description: - 'The commit SHA to reset the main branch to. Required if resetting the main branch; disallowed otherwise.', - }, - }, - required: ['project', 'branch'], - }, - annotations: { - idempotentHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { branch, ...body } = args as any; - try { - return asTextContentResult(await client.projects.branches.reset(branch, body)); - } catch (error) { - if (error instanceof Stainless.APIError) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/branches/retrieve-projects-branches.ts b/packages/mcp-server/src/tools/projects/branches/retrieve-projects-branches.ts deleted file mode 100644 index d466c0c8..00000000 --- a/packages/mcp-server/src/tools/projects/branches/retrieve-projects-branches.ts +++ /dev/null @@ -1,48 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.branches', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/projects/{project}/branches/{branch}', -}; - -export const tool: Tool = { - name: 'retrieve_projects_branches', - description: 'Retrieve a project branch by name.', - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - branch: { - type: 'string', - }, - }, - required: ['project', 'branch'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { branch, ...body } = args as any; - try { - return asTextContentResult(await client.projects.branches.retrieve(branch, body)); - } catch (error) { - if (error instanceof Stainless.APIError) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/configs/guess-projects-configs.ts b/packages/mcp-server/src/tools/projects/configs/guess-projects-configs.ts deleted file mode 100644 index c0516b9f..00000000 --- a/packages/mcp-server/src/tools/projects/configs/guess-projects-configs.ts +++ /dev/null @@ -1,59 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.configs', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v0/projects/{project}/configs/guess', -}; - -export const tool: Tool = { - name: 'guess_projects_configs', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n Generate suggestions for changes to config files based on an OpenAPI spec.\n \n\n# Response Schema\n```json\n{\n $ref: '#/$defs/config_guess_response',\n $defs: {\n config_guess_response: {\n type: 'object',\n description: 'Config files contents',\n additionalProperties: true\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - spec: { - type: 'string', - description: 'OpenAPI spec', - }, - branch: { - type: 'string', - description: 'Branch name', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['project', 'spec'], - }, - annotations: {}, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.projects.configs.guess(body))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/configs/retrieve-projects-configs.ts b/packages/mcp-server/src/tools/projects/configs/retrieve-projects-configs.ts deleted file mode 100644 index c6e73901..00000000 --- a/packages/mcp-server/src/tools/projects/configs/retrieve-projects-configs.ts +++ /dev/null @@ -1,60 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects.configs', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/projects/{project}/configs', -}; - -export const tool: Tool = { - name: 'retrieve_projects_configs', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\n\n Retrieve the configuration files for a given project.\n \n\n# Response Schema\n```json\n{\n $ref: '#/$defs/config_retrieve_response',\n $defs: {\n config_retrieve_response: {\n type: 'object',\n description: 'Config files contents',\n additionalProperties: true\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - branch: { - type: 'string', - description: 'Branch name, defaults to "main".', - }, - include: { - type: 'string', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['project'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.projects.configs.retrieve(body))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/create-projects.ts b/packages/mcp-server/src/tools/projects/create-projects.ts deleted file mode 100644 index b4c1f2e9..00000000 --- a/packages/mcp-server/src/tools/projects/create-projects.ts +++ /dev/null @@ -1,90 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects', - operation: 'write', - tags: [], - httpMethod: 'post', - httpPath: '/v0/projects', -}; - -export const tool: Tool = { - name: 'create_projects', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nCreate a new project.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/project',\n $defs: {\n project: {\n type: 'object',\n description: 'A project is a collection of SDKs generated from the same set of config files.',\n properties: {\n config_repo: {\n type: 'string'\n },\n display_name: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'project'\n ]\n },\n org: {\n type: 'string'\n },\n slug: {\n type: 'string'\n },\n targets: {\n type: 'array',\n items: {\n $ref: '#/$defs/target'\n }\n }\n },\n required: [ 'config_repo',\n 'display_name',\n 'object',\n 'org',\n 'slug',\n 'targets'\n ]\n },\n target: {\n type: 'string',\n enum: [ 'node',\n 'typescript',\n 'python',\n 'go',\n 'java',\n 'kotlin',\n 'ruby',\n 'terraform',\n 'cli',\n 'php',\n 'csharp'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - display_name: { - type: 'string', - description: 'Human-readable project name', - }, - org: { - type: 'string', - description: 'Organization name', - }, - revision: { - type: 'object', - description: 'File contents to commit', - additionalProperties: true, - }, - slug: { - type: 'string', - description: 'Project name/slug', - }, - targets: { - type: 'array', - description: 'Targets to generate for', - items: { - $ref: '#/$defs/target', - }, - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['display_name', 'org', 'revision', 'slug', 'targets'], - $defs: { - target: { - type: 'string', - enum: [ - 'node', - 'typescript', - 'python', - 'go', - 'java', - 'kotlin', - 'ruby', - 'terraform', - 'cli', - 'php', - 'csharp', - ], - }, - }, - }, - annotations: {}, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.projects.create(body))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/list-projects.ts b/packages/mcp-server/src/tools/projects/list-projects.ts deleted file mode 100644 index 829883e2..00000000 --- a/packages/mcp-server/src/tools/projects/list-projects.ts +++ /dev/null @@ -1,62 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/projects', -}; - -export const tool: Tool = { - name: 'list_projects', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nList projects in an organization, from oldest to newest.\n\n# Response Schema\n```json\n{\n type: 'object',\n properties: {\n data: {\n type: 'array',\n items: {\n $ref: '#/$defs/project'\n }\n },\n has_more: {\n type: 'boolean'\n },\n next_cursor: {\n type: 'string'\n }\n },\n required: [ 'data',\n 'has_more'\n ],\n $defs: {\n project: {\n type: 'object',\n description: 'A project is a collection of SDKs generated from the same set of config files.',\n properties: {\n config_repo: {\n type: 'string'\n },\n display_name: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'project'\n ]\n },\n org: {\n type: 'string'\n },\n slug: {\n type: 'string'\n },\n targets: {\n type: 'array',\n items: {\n $ref: '#/$defs/target'\n }\n }\n },\n required: [ 'config_repo',\n 'display_name',\n 'object',\n 'org',\n 'slug',\n 'targets'\n ]\n },\n target: {\n type: 'string',\n enum: [ 'node',\n 'typescript',\n 'python',\n 'go',\n 'java',\n 'kotlin',\n 'ruby',\n 'terraform',\n 'cli',\n 'php',\n 'csharp'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - cursor: { - type: 'string', - description: 'Pagination cursor from a previous response', - }, - limit: { - type: 'number', - description: 'Maximum number of projects to return, defaults to 10 (maximum: 100).', - }, - org: { - type: 'string', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: [], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - const response = await client.projects.list(body).asResponse(); - try { - return asTextContentResult(await maybeFilter(jq_filter, await response.json())); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/retrieve-projects.ts b/packages/mcp-server/src/tools/projects/retrieve-projects.ts deleted file mode 100644 index 75abee23..00000000 --- a/packages/mcp-server/src/tools/projects/retrieve-projects.ts +++ /dev/null @@ -1,53 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects', - operation: 'read', - tags: [], - httpMethod: 'get', - httpPath: '/v0/projects/{project}', -}; - -export const tool: Tool = { - name: 'retrieve_projects', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nRetrieve a project by name.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/project',\n $defs: {\n project: {\n type: 'object',\n description: 'A project is a collection of SDKs generated from the same set of config files.',\n properties: {\n config_repo: {\n type: 'string'\n },\n display_name: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'project'\n ]\n },\n org: {\n type: 'string'\n },\n slug: {\n type: 'string'\n },\n targets: {\n type: 'array',\n items: {\n $ref: '#/$defs/target'\n }\n }\n },\n required: [ 'config_repo',\n 'display_name',\n 'object',\n 'org',\n 'slug',\n 'targets'\n ]\n },\n target: {\n type: 'string',\n enum: [ 'node',\n 'typescript',\n 'python',\n 'go',\n 'java',\n 'kotlin',\n 'ruby',\n 'terraform',\n 'cli',\n 'php',\n 'csharp'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['project'], - }, - annotations: { - readOnlyHint: true, - }, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.projects.retrieve(body))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/projects/update-projects.ts b/packages/mcp-server/src/tools/projects/update-projects.ts deleted file mode 100644 index ff747bb4..00000000 --- a/packages/mcp-server/src/tools/projects/update-projects.ts +++ /dev/null @@ -1,54 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import { isJqError, maybeFilter } from '@stainless-api/sdk-mcp/filtering'; -import { Metadata, asErrorResult, asTextContentResult } from '@stainless-api/sdk-mcp/tools/types'; - -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import Stainless from '@stainless-api/sdk'; - -export const metadata: Metadata = { - resource: 'projects', - operation: 'write', - tags: [], - httpMethod: 'patch', - httpPath: '/v0/projects/{project}', -}; - -export const tool: Tool = { - name: 'update_projects', - description: - "When using this tool, always use the `jq_filter` parameter to reduce the response size and improve performance.\n\nOnly omit if you're sure you don't need the data.\n\nUpdate a project's properties.\n\n# Response Schema\n```json\n{\n $ref: '#/$defs/project',\n $defs: {\n project: {\n type: 'object',\n description: 'A project is a collection of SDKs generated from the same set of config files.',\n properties: {\n config_repo: {\n type: 'string'\n },\n display_name: {\n type: 'string'\n },\n object: {\n type: 'string',\n enum: [ 'project'\n ]\n },\n org: {\n type: 'string'\n },\n slug: {\n type: 'string'\n },\n targets: {\n type: 'array',\n items: {\n $ref: '#/$defs/target'\n }\n }\n },\n required: [ 'config_repo',\n 'display_name',\n 'object',\n 'org',\n 'slug',\n 'targets'\n ]\n },\n target: {\n type: 'string',\n enum: [ 'node',\n 'typescript',\n 'python',\n 'go',\n 'java',\n 'kotlin',\n 'ruby',\n 'terraform',\n 'cli',\n 'php',\n 'csharp'\n ]\n }\n }\n}\n```", - inputSchema: { - type: 'object', - properties: { - project: { - type: 'string', - }, - display_name: { - type: 'string', - }, - jq_filter: { - type: 'string', - title: 'jq Filter', - description: - 'A jq filter to apply to the response to include certain fields. Consult the output schema in the tool description to see the fields that are available.\n\nFor example: to include only the `name` field in every object of a results array, you can provide ".results[].name".\n\nFor more information, see the [jq documentation](https://jqlang.org/manual/).', - }, - }, - required: ['project'], - }, - annotations: {}, -}; - -export const handler = async (client: Stainless, args: Record | undefined) => { - const { jq_filter, ...body } = args as any; - try { - return asTextContentResult(await maybeFilter(jq_filter, await client.projects.update(body))); - } catch (error) { - if (error instanceof Stainless.APIError || isJqError(error)) { - return asErrorResult(error.message); - } - throw error; - } -}; - -export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/tools/types.ts b/packages/mcp-server/src/types.ts similarity index 98% rename from packages/mcp-server/src/tools/types.ts rename to packages/mcp-server/src/types.ts index 452cbd24..2ec1caaf 100644 --- a/packages/mcp-server/src/tools/types.ts +++ b/packages/mcp-server/src/types.ts @@ -108,7 +108,7 @@ export type Metadata = { operationId?: string; }; -export type Endpoint = { +export type McpTool = { metadata: Metadata; tool: Tool; handler: HandlerFunction; diff --git a/packages/mcp-server/tests/compat.test.ts b/packages/mcp-server/tests/compat.test.ts deleted file mode 100644 index d6272f6c..00000000 --- a/packages/mcp-server/tests/compat.test.ts +++ /dev/null @@ -1,1166 +0,0 @@ -import { - truncateToolNames, - removeTopLevelUnions, - removeAnyOf, - inlineRefs, - applyCompatibilityTransformations, - removeFormats, - findUsedDefs, -} from '../src/compat'; -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { JSONSchema } from '../src/compat'; -import { Endpoint } from '../src/tools'; - -describe('truncateToolNames', () => { - it('should return original names when maxLength is 0 or negative', () => { - const names = ['tool1', 'tool2', 'tool3']; - expect(truncateToolNames(names, 0)).toEqual(new Map()); - expect(truncateToolNames(names, -1)).toEqual(new Map()); - }); - - it('should return original names when all names are shorter than maxLength', () => { - const names = ['tool1', 'tool2', 'tool3']; - expect(truncateToolNames(names, 10)).toEqual(new Map()); - }); - - it('should truncate names longer than maxLength', () => { - const names = ['very-long-tool-name', 'another-long-tool-name', 'short']; - expect(truncateToolNames(names, 10)).toEqual( - new Map([ - ['very-long-tool-name', 'very-long-'], - ['another-long-tool-name', 'another-lo'], - ]), - ); - }); - - it('should handle duplicate truncated names by appending numbers', () => { - const names = ['tool-name-a', 'tool-name-b', 'tool-name-c']; - expect(truncateToolNames(names, 8)).toEqual( - new Map([ - ['tool-name-a', 'tool-na1'], - ['tool-name-b', 'tool-na2'], - ['tool-name-c', 'tool-na3'], - ]), - ); - }); -}); - -describe('removeTopLevelUnions', () => { - const createTestTool = (overrides = {}): Tool => ({ - name: 'test-tool', - description: 'Test tool', - inputSchema: { - type: 'object', - properties: {}, - }, - ...overrides, - }); - - it('should return the original tool if it has no anyOf at the top level', () => { - const tool = createTestTool({ - inputSchema: { - type: 'object', - properties: { - foo: { type: 'string' }, - }, - }, - }); - - expect(removeTopLevelUnions(tool)).toEqual([tool]); - }); - - it('should split a tool with top-level anyOf into multiple tools', () => { - const tool = createTestTool({ - name: 'union-tool', - description: 'A tool with unions', - inputSchema: { - type: 'object', - properties: { - common: { type: 'string' }, - }, - anyOf: [ - { - title: 'first variant', - description: 'Its the first variant', - properties: { - variant1: { type: 'string' }, - }, - required: ['variant1'], - }, - { - title: 'second variant', - properties: { - variant2: { type: 'number' }, - }, - required: ['variant2'], - }, - ], - }, - }); - - const result = removeTopLevelUnions(tool); - - expect(result).toEqual([ - { - name: 'union-tool_first_variant', - description: 'Its the first variant', - inputSchema: { - type: 'object', - title: 'first variant', - description: 'Its the first variant', - properties: { - common: { type: 'string' }, - variant1: { type: 'string' }, - }, - required: ['variant1'], - }, - }, - { - name: 'union-tool_second_variant', - description: 'A tool with unions', - inputSchema: { - type: 'object', - title: 'second variant', - description: 'A tool with unions', - properties: { - common: { type: 'string' }, - variant2: { type: 'number' }, - }, - required: ['variant2'], - }, - }, - ]); - }); - - it('should handle $defs and only include those used by the variant', () => { - const tool = createTestTool({ - name: 'defs-tool', - description: 'A tool with $defs', - inputSchema: { - type: 'object', - properties: { - common: { type: 'string' }, - }, - $defs: { - def1: { type: 'string', format: 'email' }, - def2: { type: 'number', minimum: 0 }, - unused: { type: 'boolean' }, - }, - anyOf: [ - { - properties: { - email: { $ref: '#/$defs/def1' }, - }, - }, - { - properties: { - count: { $ref: '#/$defs/def2' }, - }, - }, - ], - }, - }); - - const result = removeTopLevelUnions(tool); - - expect(result).toEqual([ - { - name: 'defs-tool_variant1', - description: 'A tool with $defs', - inputSchema: { - type: 'object', - description: 'A tool with $defs', - properties: { - common: { type: 'string' }, - email: { $ref: '#/$defs/def1' }, - }, - $defs: { - def1: { type: 'string', format: 'email' }, - }, - }, - }, - { - name: 'defs-tool_variant2', - description: 'A tool with $defs', - inputSchema: { - type: 'object', - description: 'A tool with $defs', - properties: { - common: { type: 'string' }, - count: { $ref: '#/$defs/def2' }, - }, - $defs: { - def2: { type: 'number', minimum: 0 }, - }, - }, - }, - ]); - }); -}); - -describe('removeAnyOf', () => { - it('should return original schema if it has no anyOf', () => { - const schema = { - type: 'object', - properties: { - foo: { type: 'string' }, - bar: { type: 'number' }, - }, - }; - - expect(removeAnyOf(schema)).toEqual(schema); - }); - - it('should remove anyOf field and use the first variant', () => { - const schema = { - type: 'object', - properties: { - common: { type: 'string' }, - }, - anyOf: [ - { - properties: { - variant1: { type: 'string' }, - }, - required: ['variant1'], - }, - { - properties: { - variant2: { type: 'number' }, - }, - required: ['variant2'], - }, - ], - }; - - const expected = { - type: 'object', - properties: { - common: { type: 'string' }, - variant1: { type: 'string' }, - }, - required: ['variant1'], - }; - - expect(removeAnyOf(schema)).toEqual(expected); - }); - - it('should recursively remove anyOf fields from nested properties', () => { - const schema = { - type: 'object', - properties: { - foo: { type: 'string' }, - nested: { - type: 'object', - properties: { - bar: { type: 'number' }, - }, - anyOf: [ - { - properties: { - option1: { type: 'boolean' }, - }, - }, - { - properties: { - option2: { type: 'array' }, - }, - }, - ], - }, - }, - }; - - const expected = { - type: 'object', - properties: { - foo: { type: 'string' }, - nested: { - type: 'object', - properties: { - bar: { type: 'number' }, - option1: { type: 'boolean' }, - }, - }, - }, - }; - - expect(removeAnyOf(schema)).toEqual(expected); - }); - - it('should handle arrays', () => { - const schema = { - type: 'object', - properties: { - items: { - type: 'array', - items: { - anyOf: [{ type: 'string' }, { type: 'number' }], - }, - }, - }, - }; - - const expected = { - type: 'object', - properties: { - items: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }; - - expect(removeAnyOf(schema)).toEqual(expected); - }); -}); - -describe('findUsedDefs', () => { - it('should handle circular references without stack overflow', () => { - const defs = { - person: { - type: 'object', - properties: { - name: { type: 'string' }, - friend: { $ref: '#/$defs/person' }, // Circular reference - }, - }, - }; - - const schema = { - type: 'object', - properties: { - user: { $ref: '#/$defs/person' }, - }, - }; - - // This should not throw a stack overflow error - expect(() => { - const result = findUsedDefs(schema, defs); - expect(result).toHaveProperty('person'); - }).not.toThrow(); - }); - - it('should handle indirect circular references without stack overflow', () => { - const defs = { - node: { - type: 'object', - properties: { - value: { type: 'string' }, - child: { $ref: '#/$defs/childNode' }, - }, - }, - childNode: { - type: 'object', - properties: { - value: { type: 'string' }, - parent: { $ref: '#/$defs/node' }, // Indirect circular reference - }, - }, - }; - - const schema = { - type: 'object', - properties: { - root: { $ref: '#/$defs/node' }, - }, - }; - - // This should not throw a stack overflow error - expect(() => { - const result = findUsedDefs(schema, defs); - expect(result).toHaveProperty('node'); - expect(result).toHaveProperty('childNode'); - }).not.toThrow(); - }); - - it('should find all used definitions in non-circular schemas', () => { - const defs = { - user: { - type: 'object', - properties: { - name: { type: 'string' }, - address: { $ref: '#/$defs/address' }, - }, - }, - address: { - type: 'object', - properties: { - street: { type: 'string' }, - city: { type: 'string' }, - }, - }, - unused: { - type: 'object', - properties: { - data: { type: 'string' }, - }, - }, - }; - - const schema = { - type: 'object', - properties: { - person: { $ref: '#/$defs/user' }, - }, - }; - - const result = findUsedDefs(schema, defs); - expect(result).toHaveProperty('user'); - expect(result).toHaveProperty('address'); - expect(result).not.toHaveProperty('unused'); - }); -}); - -describe('inlineRefs', () => { - it('should return the original schema if it does not contain $refs', () => { - const schema: JSONSchema = { - type: 'object', - properties: { - name: { type: 'string' }, - age: { type: 'number' }, - }, - }; - - expect(inlineRefs(schema)).toEqual(schema); - }); - - it('should inline simple $refs', () => { - const schema: JSONSchema = { - type: 'object', - properties: { - user: { $ref: '#/$defs/user' }, - }, - $defs: { - user: { - type: 'object', - properties: { - name: { type: 'string' }, - email: { type: 'string' }, - }, - }, - }, - }; - - const expected: JSONSchema = { - type: 'object', - properties: { - user: { - type: 'object', - properties: { - name: { type: 'string' }, - email: { type: 'string' }, - }, - }, - }, - }; - - expect(inlineRefs(schema)).toEqual(expected); - }); - - it('should inline nested $refs', () => { - const schema: JSONSchema = { - type: 'object', - properties: { - order: { $ref: '#/$defs/order' }, - }, - $defs: { - order: { - type: 'object', - properties: { - id: { type: 'string' }, - items: { type: 'array', items: { $ref: '#/$defs/item' } }, - }, - }, - item: { - type: 'object', - properties: { - product: { type: 'string' }, - quantity: { type: 'integer' }, - }, - }, - }, - }; - - const expected: JSONSchema = { - type: 'object', - properties: { - order: { - type: 'object', - properties: { - id: { type: 'string' }, - items: { - type: 'array', - items: { - type: 'object', - properties: { - product: { type: 'string' }, - quantity: { type: 'integer' }, - }, - }, - }, - }, - }, - }, - }; - - expect(inlineRefs(schema)).toEqual(expected); - }); - - it('should handle circular references by removing the circular part', () => { - const schema: JSONSchema = { - type: 'object', - properties: { - person: { $ref: '#/$defs/person' }, - }, - $defs: { - person: { - type: 'object', - properties: { - name: { type: 'string' }, - friend: { $ref: '#/$defs/person' }, // Circular reference - }, - }, - }, - }; - - const expected: JSONSchema = { - type: 'object', - properties: { - person: { - type: 'object', - properties: { - name: { type: 'string' }, - // friend property is removed to break the circular reference - }, - }, - }, - }; - - expect(inlineRefs(schema)).toEqual(expected); - }); - - it('should handle indirect circular references', () => { - const schema: JSONSchema = { - type: 'object', - properties: { - node: { $ref: '#/$defs/node' }, - }, - $defs: { - node: { - type: 'object', - properties: { - value: { type: 'string' }, - child: { $ref: '#/$defs/childNode' }, - }, - }, - childNode: { - type: 'object', - properties: { - value: { type: 'string' }, - parent: { $ref: '#/$defs/node' }, // Circular reference through childNode - }, - }, - }, - }; - - const expected: JSONSchema = { - type: 'object', - properties: { - node: { - type: 'object', - properties: { - value: { type: 'string' }, - child: { - type: 'object', - properties: { - value: { type: 'string' }, - // parent property is removed to break the circular reference - }, - }, - }, - }, - }, - }; - - expect(inlineRefs(schema)).toEqual(expected); - }); - - it('should preserve other properties when inlining references', () => { - const schema: JSONSchema = { - type: 'object', - properties: { - address: { $ref: '#/$defs/address', description: 'User address' }, - }, - $defs: { - address: { - type: 'object', - properties: { - street: { type: 'string' }, - city: { type: 'string' }, - }, - required: ['street'], - }, - }, - }; - - const expected: JSONSchema = { - type: 'object', - properties: { - address: { - type: 'object', - description: 'User address', - properties: { - street: { type: 'string' }, - city: { type: 'string' }, - }, - required: ['street'], - }, - }, - }; - - expect(inlineRefs(schema)).toEqual(expected); - }); -}); - -describe('removeFormats', () => { - it('should return original schema if formats capability is true', () => { - const schema = { - type: 'object', - properties: { - date: { type: 'string', description: 'A date field', format: 'date' }, - email: { type: 'string', description: 'An email field', format: 'email' }, - }, - }; - - expect(removeFormats(schema, true)).toEqual(schema); - }); - - it('should move format to description when formats capability is false', () => { - const schema = { - type: 'object', - properties: { - date: { type: 'string', description: 'A date field', format: 'date' }, - email: { type: 'string', description: 'An email field', format: 'email' }, - }, - }; - - const expected = { - type: 'object', - properties: { - date: { type: 'string', description: 'A date field (format: "date")' }, - email: { type: 'string', description: 'An email field (format: "email")' }, - }, - }; - - expect(removeFormats(schema, false)).toEqual(expected); - }); - - it('should handle properties without description', () => { - const schema = { - type: 'object', - properties: { - date: { type: 'string', format: 'date' }, - }, - }; - - const expected = { - type: 'object', - properties: { - date: { type: 'string', description: '(format: "date")' }, - }, - }; - - expect(removeFormats(schema, false)).toEqual(expected); - }); - - it('should handle nested properties', () => { - const schema = { - type: 'object', - properties: { - user: { - type: 'object', - properties: { - created_at: { type: 'string', description: 'Creation date', format: 'date-time' }, - }, - }, - }, - }; - - const expected = { - type: 'object', - properties: { - user: { - type: 'object', - properties: { - created_at: { type: 'string', description: 'Creation date (format: "date-time")' }, - }, - }, - }, - }; - - expect(removeFormats(schema, false)).toEqual(expected); - }); - - it('should handle arrays of objects', () => { - const schema = { - type: 'object', - properties: { - dates: { - type: 'array', - items: { - type: 'object', - properties: { - start: { type: 'string', description: 'Start date', format: 'date' }, - end: { type: 'string', description: 'End date', format: 'date' }, - }, - }, - }, - }, - }; - - const expected = { - type: 'object', - properties: { - dates: { - type: 'array', - items: { - type: 'object', - properties: { - start: { type: 'string', description: 'Start date (format: "date")' }, - end: { type: 'string', description: 'End date (format: "date")' }, - }, - }, - }, - }, - }; - - expect(removeFormats(schema, false)).toEqual(expected); - }); - - it('should handle schemas with $defs', () => { - const schema = { - type: 'object', - properties: { - date: { type: 'string', description: 'A date field', format: 'date' }, - }, - $defs: { - timestamp: { - type: 'string', - description: 'A timestamp field', - format: 'date-time', - }, - }, - }; - - const expected = { - type: 'object', - properties: { - date: { type: 'string', description: 'A date field (format: "date")' }, - }, - $defs: { - timestamp: { - type: 'string', - description: 'A timestamp field (format: "date-time")', - }, - }, - }; - - expect(removeFormats(schema, false)).toEqual(expected); - }); -}); - -describe('applyCompatibilityTransformations', () => { - const createTestTool = (name: string, overrides = {}): Tool => ({ - name, - description: 'Test tool', - inputSchema: { - type: 'object', - properties: {}, - }, - ...overrides, - }); - - const createTestEndpoint = (tool: Tool): Endpoint => ({ - tool, - handler: jest.fn(), - metadata: { - resource: 'test', - operation: 'read' as const, - tags: [], - }, - }); - - it('should not modify endpoints when all capabilities are enabled', () => { - const tool = createTestTool('test-tool'); - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - expect(transformed).toEqual(endpoints); - }); - - it('should split tools with top-level unions when topLevelUnions is disabled', () => { - const tool = createTestTool('union-tool', { - inputSchema: { - type: 'object', - properties: { - common: { type: 'string' }, - }, - anyOf: [ - { - title: 'first variant', - properties: { - variant1: { type: 'string' }, - }, - }, - { - title: 'second variant', - properties: { - variant2: { type: 'number' }, - }, - }, - ], - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: false, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - expect(transformed.length).toBe(2); - expect(transformed[0]!.tool.name).toBe('union-tool_first_variant'); - expect(transformed[1]!.tool.name).toBe('union-tool_second_variant'); - }); - - it('should handle variants without titles in removeTopLevelUnions', () => { - const tool = createTestTool('union-tool', { - inputSchema: { - type: 'object', - properties: { - common: { type: 'string' }, - }, - anyOf: [ - { - properties: { - variant1: { type: 'string' }, - }, - }, - { - properties: { - variant2: { type: 'number' }, - }, - }, - ], - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: false, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - expect(transformed.length).toBe(2); - expect(transformed[0]!.tool.name).toBe('union-tool_variant1'); - expect(transformed[1]!.tool.name).toBe('union-tool_variant2'); - }); - - it('should truncate tool names when toolNameLength is set', () => { - const tools = [ - createTestTool('very-long-tool-name-that-exceeds-limit'), - createTestTool('another-long-tool-name-to-truncate'), - createTestTool('short-name'), - ]; - - const endpoints = tools.map(createTestEndpoint); - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: 20, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - expect(transformed[0]!.tool.name).toBe('very-long-tool-name-'); - expect(transformed[1]!.tool.name).toBe('another-long-tool-na'); - expect(transformed[2]!.tool.name).toBe('short-name'); - }); - - it('should inline refs when refs capability is disabled', () => { - const tool = createTestTool('ref-tool', { - inputSchema: { - type: 'object', - properties: { - user: { $ref: '#/$defs/user' }, - }, - $defs: { - user: { - type: 'object', - properties: { - name: { type: 'string' }, - email: { type: 'string' }, - }, - }, - }, - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: false, - unions: true, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - const schema = transformed[0]!.tool.inputSchema as JSONSchema; - expect(schema.$defs).toBeUndefined(); - - if (schema.properties) { - expect(schema.properties['user']).toEqual({ - type: 'object', - properties: { - name: { type: 'string' }, - email: { type: 'string' }, - }, - }); - } - }); - - it('should preserve external refs when inlining', () => { - const tool = createTestTool('ref-tool', { - inputSchema: { - type: 'object', - properties: { - internal: { $ref: '#/$defs/internal' }, - external: { $ref: 'https://example.com/schemas/external.json' }, - }, - $defs: { - internal: { - type: 'object', - properties: { - name: { type: 'string' }, - }, - }, - }, - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: false, - unions: true, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - const schema = transformed[0]!.tool.inputSchema as JSONSchema; - - if (schema.properties) { - expect(schema.properties['internal']).toEqual({ - type: 'object', - properties: { - name: { type: 'string' }, - }, - }); - expect(schema.properties['external']).toEqual({ - $ref: 'https://example.com/schemas/external.json', - }); - } - }); - - it('should remove anyOf fields when unions capability is disabled', () => { - const tool = createTestTool('union-tool', { - inputSchema: { - type: 'object', - properties: { - field: { - anyOf: [{ type: 'string' }, { type: 'number' }], - }, - }, - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: false, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - const schema = transformed[0]!.tool.inputSchema as JSONSchema; - - if (schema.properties && schema.properties['field']) { - const field = schema.properties['field']; - expect(field.anyOf).toBeUndefined(); - expect(field.type).toBe('string'); - } - }); - - it('should correctly combine topLevelUnions and toolNameLength transformations', () => { - const tool = createTestTool('very-long-union-tool-name', { - inputSchema: { - type: 'object', - properties: { - common: { type: 'string' }, - }, - anyOf: [ - { - title: 'first variant', - properties: { - variant1: { type: 'string' }, - }, - }, - { - title: 'second variant', - properties: { - variant2: { type: 'number' }, - }, - }, - ], - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: false, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: 20, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - expect(transformed.length).toBe(2); - - // Both names should be truncated because they exceed 20 characters - expect(transformed[0]!.tool.name).toBe('very-long-union-too1'); - expect(transformed[1]!.tool.name).toBe('very-long-union-too2'); - }); - - it('should correctly combine refs and unions transformations', () => { - const tool = createTestTool('complex-tool', { - inputSchema: { - type: 'object', - properties: { - user: { $ref: '#/$defs/user' }, - }, - $defs: { - user: { - type: 'object', - properties: { - preference: { - anyOf: [{ type: 'string' }, { type: 'number' }], - }, - }, - }, - }, - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: false, - unions: false, - formats: true, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - const schema = transformed[0]!.tool.inputSchema as JSONSchema; - - // Refs should be inlined - expect(schema.$defs).toBeUndefined(); - - // Safely access nested properties - if (schema.properties && schema.properties['user']) { - const user = schema.properties['user']; - // User should be inlined - expect(user.type).toBe('object'); - - // AnyOf in the inlined user.preference should be removed - if (user.properties && user.properties['preference']) { - const preference = user.properties['preference']; - expect(preference.anyOf).toBeUndefined(); - expect(preference.type).toBe('string'); - } - } - }); - - it('should handle formats capability being false', () => { - const tool = createTestTool('format-tool', { - inputSchema: { - type: 'object', - properties: { - date: { type: 'string', description: 'A date', format: 'date' }, - }, - }, - }); - - const endpoints = [createTestEndpoint(tool)]; - - const capabilities = { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: false, - toolNameLength: undefined, - }; - - const transformed = applyCompatibilityTransformations(endpoints, capabilities); - const schema = transformed[0]!.tool.inputSchema as JSONSchema; - - if (schema.properties && schema.properties['date']) { - const dateField = schema.properties['date']; - expect(dateField['format']).toBeUndefined(); - expect(dateField['description']).toBe('A date (format: "date")'); - } - }); -}); diff --git a/packages/mcp-server/tests/dynamic-tools.test.ts b/packages/mcp-server/tests/dynamic-tools.test.ts deleted file mode 100644 index 08963af8..00000000 --- a/packages/mcp-server/tests/dynamic-tools.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { dynamicTools } from '../src/dynamic-tools'; -import { Endpoint } from '../src/tools'; - -describe('dynamicTools', () => { - const fakeClient = {} as any; - - const endpoints: Endpoint[] = [ - makeEndpoint('test_read_endpoint', 'test_resource', 'read', ['test']), - makeEndpoint('test_write_endpoint', 'test_resource', 'write', ['test']), - makeEndpoint('user_endpoint', 'user', 'read', ['user', 'admin']), - makeEndpoint('admin_endpoint', 'admin', 'write', ['admin']), - ]; - - const tools = dynamicTools(endpoints); - - const toolsMap = { - list_api_endpoints: toolOrError('list_api_endpoints'), - get_api_endpoint_schema: toolOrError('get_api_endpoint_schema'), - invoke_api_endpoint: toolOrError('invoke_api_endpoint'), - }; - - describe('list_api_endpoints', () => { - it('should return all endpoints when no search query is provided', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, {}); - const result = JSON.parse(content.content[0].text); - - expect(result.tools).toHaveLength(endpoints.length); - expect(result.tools.map((t: { name: string }) => t.name)).toContain('test_read_endpoint'); - expect(result.tools.map((t: { name: string }) => t.name)).toContain('test_write_endpoint'); - expect(result.tools.map((t: { name: string }) => t.name)).toContain('user_endpoint'); - expect(result.tools.map((t: { name: string }) => t.name)).toContain('admin_endpoint'); - }); - - it('should filter endpoints by name', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'user' }); - const result = JSON.parse(content.content[0].text); - - expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('user_endpoint'); - }); - - it('should filter endpoints by resource', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'admin' }); - const result = JSON.parse(content.content[0].text); - - expect(result.tools.some((t: { resource: string }) => t.resource === 'admin')).toBeTruthy(); - }); - - it('should filter endpoints by tag', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'admin' }); - const result = JSON.parse(content.content[0].text); - - expect(result.tools.some((t: { tags: string[] }) => t.tags.includes('admin'))).toBeTruthy(); - }); - - it('should be case insensitive in search', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, { search_query: 'ADMIN' }); - const result = JSON.parse(content.content[0].text); - - expect(result.tools.length).toBe(2); - result.tools.forEach((tool: { name: string; resource: string; tags: string[] }) => { - expect( - tool.name.toLowerCase().includes('admin') || - tool.resource.toLowerCase().includes('admin') || - tool.tags.some((tag: string) => tag.toLowerCase().includes('admin')), - ).toBeTruthy(); - }); - }); - - it('should filter endpoints by description', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, { - search_query: 'Test endpoint for user_endpoint', - }); - const result = JSON.parse(content.content[0].text); - - expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('user_endpoint'); - expect(result.tools[0].description).toBe('Test endpoint for user_endpoint'); - }); - - it('should filter endpoints by partial description match', async () => { - const content = await toolsMap.list_api_endpoints.handler(fakeClient, { - search_query: 'endpoint for user', - }); - const result = JSON.parse(content.content[0].text); - - expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe('user_endpoint'); - }); - }); - - describe('get_api_endpoint_schema', () => { - it('should return schema for existing endpoint', async () => { - const content = await toolsMap.get_api_endpoint_schema.handler(fakeClient, { - endpoint: 'test_read_endpoint', - }); - const result = JSON.parse(content.content[0].text); - - expect(result).toEqual(endpoints[0]?.tool); - }); - - it('should throw error for non-existent endpoint', async () => { - await expect( - toolsMap.get_api_endpoint_schema.handler(fakeClient, { endpoint: 'non_existent_endpoint' }), - ).rejects.toThrow('Endpoint non_existent_endpoint not found'); - }); - - it('should throw error when no endpoint provided', async () => { - await expect(toolsMap.get_api_endpoint_schema.handler(fakeClient, undefined)).rejects.toThrow( - 'No endpoint provided', - ); - }); - }); - - describe('invoke_api_endpoint', () => { - it('should successfully invoke endpoint with valid arguments', async () => { - const mockHandler = endpoints[0]?.handler as jest.Mock; - mockHandler.mockClear(); - - await toolsMap.invoke_api_endpoint.handler(fakeClient, { - endpoint_name: 'test_read_endpoint', - args: { testParam: 'test value' }, - }); - - expect(mockHandler).toHaveBeenCalledWith(fakeClient, { testParam: 'test value' }); - }); - - it('should throw error for non-existent endpoint', async () => { - await expect( - toolsMap.invoke_api_endpoint.handler(fakeClient, { - endpoint_name: 'non_existent_endpoint', - args: { testParam: 'test value' }, - }), - ).rejects.toThrow(/Endpoint non_existent_endpoint not found/); - }); - - it('should throw error when no arguments provided', async () => { - await expect(toolsMap.invoke_api_endpoint.handler(fakeClient, undefined)).rejects.toThrow( - 'No endpoint provided', - ); - }); - - it('should throw error for invalid argument schema', async () => { - await expect( - toolsMap.invoke_api_endpoint.handler(fakeClient, { - endpoint_name: 'test_read_endpoint', - args: { wrongParam: 'test value' }, // Missing required testParam - }), - ).rejects.toThrow(/Invalid arguments for endpoint/); - }); - }); - - function toolOrError(name: string) { - const tool = tools.find((tool) => tool.tool.name === name); - if (!tool) throw new Error(`Tool ${name} not found`); - return tool; - } -}); - -function makeEndpoint( - name: string, - resource: string, - operation: 'read' | 'write', - tags: string[] = [], -): Endpoint { - return { - metadata: { - resource, - operation, - tags, - }, - tool: { - name, - description: `Test endpoint for ${name}`, - inputSchema: { - type: 'object', - properties: { - testParam: { type: 'string' }, - }, - required: ['testParam'], - }, - }, - handler: jest.fn().mockResolvedValue({ success: true }), - }; -} diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 4d9b60ca..532666a8 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -1,6 +1,4 @@ import { parseCLIOptions, parseQueryOptions } from '../src/options'; -import { Filter } from '../src/tools'; -import { parseEmbeddedJSON } from '../src/compat'; // Mock process.argv const mockArgv = (args: string[]) => { @@ -12,338 +10,35 @@ const mockArgv = (args: string[]) => { }; describe('parseCLIOptions', () => { - it('should parse basic filter options', () => { - const cleanup = mockArgv([ - '--tool=test-tool', - '--resource=test-resource', - '--operation=read', - '--tag=test-tag', - ]); + it('default parsing should be stdio', () => { + const cleanup = mockArgv([]); const result = parseCLIOptions(); - expect(result.filters).toEqual([ - { type: 'tag', op: 'include', value: 'test-tag' }, - { type: 'resource', op: 'include', value: 'test-resource' }, - { type: 'tool', op: 'include', value: 'test-tool' }, - { type: 'operation', op: 'include', value: 'read' }, - ] as Filter[]); - - expect(result.capabilities).toEqual({}); - - expect(result.list).toBe(false); + expect(result.transport).toBe('stdio'); cleanup(); }); - it('should parse exclusion filters', () => { - const cleanup = mockArgv([ - '--no-tool=exclude-tool', - '--no-resource=exclude-resource', - '--no-operation=write', - '--no-tag=exclude-tag', - ]); + it('using http transport with a port', () => { + const cleanup = mockArgv(['--transport=http', '--port=2222']); const result = parseCLIOptions(); - expect(result.filters).toEqual([ - { type: 'tag', op: 'exclude', value: 'exclude-tag' }, - { type: 'resource', op: 'exclude', value: 'exclude-resource' }, - { type: 'tool', op: 'exclude', value: 'exclude-tool' }, - { type: 'operation', op: 'exclude', value: 'write' }, - ] as Filter[]); - - expect(result.capabilities).toEqual({}); - - cleanup(); - }); - - it('should parse client presets', () => { - const cleanup = mockArgv(['--client=openai-agents']); - - const result = parseCLIOptions(); - - expect(result.client).toEqual('openai-agents'); - - cleanup(); - }); - - it('should parse individual capabilities', () => { - const cleanup = mockArgv([ - '--capability=top-level-unions', - '--capability=valid-json', - '--capability=refs', - '--capability=unions', - '--capability=tool-name-length=40', - ]); - - const result = parseCLIOptions(); - - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - toolNameLength: 40, - }); - - cleanup(); - }); - - it('should handle list option', () => { - const cleanup = mockArgv(['--list']); - - const result = parseCLIOptions(); - - expect(result.list).toBe(true); - - cleanup(); - }); - - it('should handle multiple filters of the same type', () => { - const cleanup = mockArgv(['--tool=tool1', '--tool=tool2', '--resource=res1', '--resource=res2']); - - const result = parseCLIOptions(); - - expect(result.filters).toEqual([ - { type: 'resource', op: 'include', value: 'res1' }, - { type: 'resource', op: 'include', value: 'res2' }, - { type: 'tool', op: 'include', value: 'tool1' }, - { type: 'tool', op: 'include', value: 'tool2' }, - ] as Filter[]); - - cleanup(); - }); - - it('should handle comma-separated values in array options', () => { - const cleanup = mockArgv([ - '--tool=tool1,tool2', - '--resource=res1,res2', - '--capability=top-level-unions,valid-json,unions', - ]); - - const result = parseCLIOptions(); - - expect(result.filters).toEqual([ - { type: 'resource', op: 'include', value: 'res1' }, - { type: 'resource', op: 'include', value: 'res2' }, - { type: 'tool', op: 'include', value: 'tool1' }, - { type: 'tool', op: 'include', value: 'tool2' }, - ] as Filter[]); - - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: true, - unions: true, - }); - - cleanup(); - }); - - it('should handle invalid tool-name-length format', () => { - const cleanup = mockArgv(['--capability=tool-name-length=invalid']); - - // Mock console.error to prevent output during test - const originalError = console.error; - console.error = jest.fn(); - - expect(() => parseCLIOptions()).toThrow(); - - console.error = originalError; - cleanup(); - }); - - it('should handle unknown capability', () => { - const cleanup = mockArgv(['--capability=unknown-capability']); - - // Mock console.error to prevent output during test - const originalError = console.error; - console.error = jest.fn(); - - expect(() => parseCLIOptions()).toThrow(); - - console.error = originalError; + expect(result.transport).toBe('http'); + expect(result.port).toBe('2222'); cleanup(); }); }); describe('parseQueryOptions', () => { - const defaultOptions = { - client: undefined, - includeDynamicTools: undefined, - includeCodeTools: undefined, - includeAllTools: undefined, - filters: [], - capabilities: { - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }, - }; - - it('should parse basic filter options from query string', () => { - const query = 'tool=test-tool&resource=test-resource&operation=read&tag=test-tag'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.filters).toEqual([ - { type: 'resource', op: 'include', value: 'test-resource' }, - { type: 'operation', op: 'include', value: 'read' }, - { type: 'tag', op: 'include', value: 'test-tag' }, - { type: 'tool', op: 'include', value: 'test-tool' }, - ]); - - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: undefined, - }); - }); - - it('should parse exclusion filters from query string', () => { - const query = 'no_tool=exclude-tool&no_resource=exclude-resource&no_operation=write&no_tag=exclude-tag'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.filters).toEqual([ - { type: 'resource', op: 'exclude', value: 'exclude-resource' }, - { type: 'operation', op: 'exclude', value: 'write' }, - { type: 'tag', op: 'exclude', value: 'exclude-tag' }, - { type: 'tool', op: 'exclude', value: 'exclude-tool' }, - ]); - }); - - it('should parse client option from query string', () => { - const query = 'client=openai-agents'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.client).toBe('openai-agents'); - }); - - it('should parse client capabilities from query string', () => { - const query = 'capability=top-level-unions&capability=valid-json&capability=tool-name-length%3D40'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: true, - refs: true, - unions: true, - formats: true, - toolNameLength: 40, - }); - }); - - it('should parse no-capability options from query string', () => { - const query = 'no_capability=top-level-unions&no_capability=refs&no_capability=formats'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.capabilities).toEqual({ - topLevelUnions: false, - validJson: true, - refs: false, - unions: true, - formats: false, - toolNameLength: undefined, - }); - }); - - it('should parse tools options from query string', () => { - const query = 'tools=dynamic&tools=all'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.includeDynamicTools).toBe(true); - expect(result.includeAllTools).toBe(true); - }); - - it('should parse no-tools options from query string', () => { - const query = 'tools=dynamic&tools=all&no_tools=dynamic'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.includeDynamicTools).toBe(false); - expect(result.includeAllTools).toBe(true); - }); - - it('should handle array values in query string', () => { - const query = 'tool[]=tool1&tool[]=tool2&resource[]=res1&resource[]=res2'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.filters).toEqual([ - { type: 'resource', op: 'include', value: 'res1' }, - { type: 'resource', op: 'include', value: 'res2' }, - { type: 'tool', op: 'include', value: 'tool1' }, - { type: 'tool', op: 'include', value: 'tool2' }, - ]); - }); - - it('should merge with default options', () => { - const defaultWithFilters = { - ...defaultOptions, - filters: [{ type: 'tag' as const, op: 'include' as const, value: 'existing-tag' }], - client: 'cursor' as const, - includeDynamicTools: true, - }; - - const query = 'tool=new-tool&resource=new-resource'; - const result = parseQueryOptions(defaultWithFilters, query); - - expect(result.filters).toEqual([ - { type: 'tag', op: 'include', value: 'existing-tag' }, - { type: 'resource', op: 'include', value: 'new-resource' }, - { type: 'tool', op: 'include', value: 'new-tool' }, - ]); - - expect(result.client).toBe('cursor'); - expect(result.includeDynamicTools).toBe(true); - }); - - it('should override client from default options', () => { - const defaultWithClient = { - ...defaultOptions, - client: 'cursor' as const, - }; + const defaultOptions = {}; - const query = 'client=openai-agents'; - const result = parseQueryOptions(defaultWithClient, query); - - expect(result.client).toBe('openai-agents'); - }); - - it('should merge capabilities with default options', () => { - const defaultWithCapabilities = { - ...defaultOptions, - capabilities: { - topLevelUnions: false, - validJson: false, - refs: true, - unions: true, - formats: true, - toolNameLength: 30, - }, - }; - - const query = 'capability=top-level-unions&no_capability=refs'; - const result = parseQueryOptions(defaultWithCapabilities, query); - - expect(result.capabilities).toEqual({ - topLevelUnions: true, - validJson: false, - refs: false, - unions: true, - formats: true, - toolNameLength: 30, - }); - }); - - it('should handle empty query string', () => { + it('default parsing should be empty', () => { const query = ''; const result = parseQueryOptions(defaultOptions, query); - expect(result).toEqual(defaultOptions); + expect(result).toBe({}); }); it('should handle invalid query string gracefully', () => { @@ -352,189 +47,4 @@ describe('parseQueryOptions', () => { // Should throw due to Zod validation for invalid operation expect(() => parseQueryOptions(defaultOptions, query)).toThrow(); }); - - it('should preserve default undefined values when not specified', () => { - const defaultWithUndefined = { - ...defaultOptions, - client: undefined, - includeDynamicTools: undefined, - includeAllTools: undefined, - }; - - const query = 'tool=test-tool'; - const result = parseQueryOptions(defaultWithUndefined, query); - - expect(result.client).toBeUndefined(); - expect(result.includeDynamicTools).toBeFalsy(); - expect(result.includeAllTools).toBeFalsy(); - }); - - it('should handle complex query with mixed include and exclude filters', () => { - const query = - 'tool=include-tool&no_tool=exclude-tool&resource=include-res&no_resource=exclude-res&operation=read&tag=include-tag&no_tag=exclude-tag'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.filters).toEqual([ - { type: 'resource', op: 'include', value: 'include-res' }, - { type: 'operation', op: 'include', value: 'read' }, - { type: 'tag', op: 'include', value: 'include-tag' }, - { type: 'tool', op: 'include', value: 'include-tool' }, - { type: 'resource', op: 'exclude', value: 'exclude-res' }, - { type: 'tag', op: 'exclude', value: 'exclude-tag' }, - { type: 'tool', op: 'exclude', value: 'exclude-tool' }, - ]); - }); - - it('code tools are enabled on http servers with default option set', () => { - const query = 'tools=code'; - const result = parseQueryOptions({ ...defaultOptions, includeCodeTools: true }, query); - - expect(result.includeCodeTools).toBe(true); - }); - - it('code tools are prevented on http servers when no default option set', () => { - const query = 'tools=code'; - const result = parseQueryOptions(defaultOptions, query); - - expect(result.includeCodeTools).toBe(undefined); - }); - - it('code tools are prevented on http servers when default option is explicitly false', () => { - const query = 'tools=code'; - const result = parseQueryOptions({ ...defaultOptions, includeCodeTools: false }, query); - - expect(result.includeCodeTools).toBe(false); - }); -}); - -describe('parseEmbeddedJSON', () => { - it('should not change non-string values', () => { - const args = { - numberProp: 42, - booleanProp: true, - objectProp: { nested: 'value' }, - arrayProp: [1, 2, 3], - nullProp: null, - undefinedProp: undefined, - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).toBe(args); // Should return original object since no changes made - expect(result['numberProp']).toBe(42); - expect(result['booleanProp']).toBe(true); - expect(result['objectProp']).toEqual({ nested: 'value' }); - expect(result['arrayProp']).toEqual([1, 2, 3]); - expect(result['nullProp']).toBe(null); - expect(result['undefinedProp']).toBe(undefined); - }); - - it('should parse valid JSON objects in string properties', () => { - const args = { - jsonObjectString: '{"key": "value", "number": 123}', - regularString: 'not json', - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).not.toBe(args); // Should return new object since changes were made - expect(result['jsonObjectString']).toEqual({ key: 'value', number: 123 }); - expect(result['regularString']).toBe('not json'); - }); - - it('should leave invalid JSON in string properties unchanged', () => { - const args = { - invalidJson1: '{"key": value}', // Missing quotes around value - invalidJson2: '{key: "value"}', // Missing quotes around key - invalidJson3: '{"key": "value",}', // Trailing comma - invalidJson4: 'just a regular string', - emptyString: '', - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).toBe(args); // Should return original object since no changes made - expect(result['invalidJson1']).toBe('{"key": value}'); - expect(result['invalidJson2']).toBe('{key: "value"}'); - expect(result['invalidJson3']).toBe('{"key": "value",}'); - expect(result['invalidJson4']).toBe('just a regular string'); - expect(result['emptyString']).toBe(''); - }); - - it('should not parse JSON primitives in string properties', () => { - const args = { - numberString: '123', - floatString: '45.67', - negativeNumberString: '-89', - booleanTrueString: 'true', - booleanFalseString: 'false', - nullString: 'null', - jsonArrayString: '[1, 2, 3, "test"]', - regularString: 'not json', - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).toBe(args); // Should return original object since no changes made - expect(result['numberString']).toBe('123'); - expect(result['floatString']).toBe('45.67'); - expect(result['negativeNumberString']).toBe('-89'); - expect(result['booleanTrueString']).toBe('true'); - expect(result['booleanFalseString']).toBe('false'); - expect(result['nullString']).toBe('null'); - expect(result['jsonArrayString']).toBe('[1, 2, 3, "test"]'); - expect(result['regularString']).toBe('not json'); - }); - - it('should handle mixed valid objects and other JSON types', () => { - const args = { - validObject: '{"success": true}', - invalidObject: '{"missing": quote}', - validNumber: '42', - validArray: '[1, 2, 3]', - keepAsString: 'hello world', - nonString: 123, - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).not.toBe(args); // Should return new object since some changes were made - expect(result['validObject']).toEqual({ success: true }); - expect(result['invalidObject']).toBe('{"missing": quote}'); - expect(result['validNumber']).toBe('42'); // Not parsed, remains string - expect(result['validArray']).toBe('[1, 2, 3]'); // Not parsed, remains string - expect(result['keepAsString']).toBe('hello world'); - expect(result['nonString']).toBe(123); - }); - - it('should return original object when no strings are present', () => { - const args = { - number: 42, - boolean: true, - object: { key: 'value' }, - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).toBe(args); // Should return original object since no changes made - }); - - it('should return original object when all strings are invalid JSON', () => { - const args = { - string1: 'hello', - string2: 'world', - string3: 'not json at all', - }; - const schema = {}; - - const result = parseEmbeddedJSON(args, schema); - - expect(result).toBe(args); // Should return original object since no changes made - }); }); diff --git a/packages/mcp-server/tests/tools.test.ts b/packages/mcp-server/tests/tools.test.ts deleted file mode 100644 index cfff24a2..00000000 --- a/packages/mcp-server/tests/tools.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { Endpoint, Filter, Metadata, query } from '../src/tools'; - -describe('Endpoint filtering', () => { - const endpoints: Endpoint[] = [ - endpoint({ - resource: 'user', - operation: 'read', - tags: ['admin'], - toolName: 'retrieve_user', - }), - endpoint({ - resource: 'user.profile', - operation: 'write', - tags: [], - toolName: 'create_user_profile', - }), - endpoint({ - resource: 'user.profile', - operation: 'read', - tags: [], - toolName: 'get_user_profile', - }), - endpoint({ - resource: 'user.roles.permissions', - operation: 'write', - tags: ['admin', 'security'], - toolName: 'update_user_role_permissions', - }), - endpoint({ - resource: 'documents.metadata.tags', - operation: 'write', - tags: ['taxonomy', 'metadata'], - toolName: 'create_document_metadata_tags', - }), - endpoint({ - resource: 'organization.settings', - operation: 'read', - tags: ['admin', 'configuration'], - toolName: 'get_organization_settings', - }), - ]; - - const tests: { name: string; filters: Filter[]; expected: string[] }[] = [ - { - name: 'match none', - filters: [], - expected: [], - }, - - // Resource tests - { - name: 'simple resource', - filters: [{ type: 'resource', op: 'include', value: 'user' }], - expected: ['retrieve_user'], - }, - { - name: 'exclude resource', - filters: [{ type: 'resource', op: 'exclude', value: 'user' }], - expected: [ - 'create_user_profile', - 'get_user_profile', - 'update_user_role_permissions', - 'create_document_metadata_tags', - 'get_organization_settings', - ], - }, - { - name: 'resource and subresources', - filters: [{ type: 'resource', op: 'include', value: 'user*' }], - expected: ['retrieve_user', 'create_user_profile', 'get_user_profile', 'update_user_role_permissions'], - }, - { - name: 'just subresources', - filters: [{ type: 'resource', op: 'include', value: 'user.*' }], - expected: ['create_user_profile', 'get_user_profile', 'update_user_role_permissions'], - }, - { - name: 'specific subresource', - filters: [{ type: 'resource', op: 'include', value: 'user.roles.permissions' }], - expected: ['update_user_role_permissions'], - }, - { - name: 'deep wildcard match', - filters: [{ type: 'resource', op: 'include', value: '*.*.tags' }], - expected: ['create_document_metadata_tags'], - }, - - // Operation tests - { - name: 'read operation', - filters: [{ type: 'operation', op: 'include', value: 'read' }], - expected: ['retrieve_user', 'get_user_profile', 'get_organization_settings'], - }, - { - name: 'write operation', - filters: [{ type: 'operation', op: 'include', value: 'write' }], - expected: ['create_user_profile', 'update_user_role_permissions', 'create_document_metadata_tags'], - }, - { - name: 'resource and operation combined', - filters: [ - { type: 'resource', op: 'include', value: 'user.profile' }, - { type: 'operation', op: 'exclude', value: 'write' }, - ], - expected: ['get_user_profile'], - }, - - // Tag tests - { - name: 'admin tag', - filters: [{ type: 'tag', op: 'include', value: 'admin' }], - expected: ['retrieve_user', 'update_user_role_permissions', 'get_organization_settings'], - }, - { - name: 'taxonomy tag', - filters: [{ type: 'tag', op: 'include', value: 'taxonomy' }], - expected: ['create_document_metadata_tags'], - }, - { - name: 'multiple tags (OR logic)', - filters: [ - { type: 'tag', op: 'include', value: 'admin' }, - { type: 'tag', op: 'include', value: 'security' }, - ], - expected: ['retrieve_user', 'update_user_role_permissions', 'get_organization_settings'], - }, - { - name: 'excluding a tag', - filters: [ - { type: 'tag', op: 'include', value: 'admin' }, - { type: 'tag', op: 'exclude', value: 'security' }, - ], - expected: ['retrieve_user', 'get_organization_settings'], - }, - - // Tool name tests - { - name: 'tool name match', - filters: [{ type: 'tool', op: 'include', value: 'get_organization_settings' }], - expected: ['get_organization_settings'], - }, - { - name: 'two tools match', - filters: [ - { type: 'tool', op: 'include', value: 'get_organization_settings' }, - { type: 'tool', op: 'include', value: 'create_user_profile' }, - ], - expected: ['create_user_profile', 'get_organization_settings'], - }, - { - name: 'excluding tool by name', - filters: [ - { type: 'resource', op: 'include', value: 'user*' }, - { type: 'tool', op: 'exclude', value: 'retrieve_user' }, - ], - expected: ['create_user_profile', 'get_user_profile', 'update_user_role_permissions'], - }, - - // Complex combinations - { - name: 'complex filter: read operations with admin tag', - filters: [ - { type: 'operation', op: 'include', value: 'read' }, - { type: 'tag', op: 'include', value: 'admin' }, - ], - expected: [ - 'retrieve_user', - 'get_user_profile', - 'update_user_role_permissions', - 'get_organization_settings', - ], - }, - { - name: 'complex filter: user resources with no tags', - filters: [ - { type: 'resource', op: 'include', value: 'user.profile' }, - { type: 'tag', op: 'exclude', value: 'admin' }, - ], - expected: ['create_user_profile', 'get_user_profile'], - }, - { - name: 'complex filter: user resources and tags', - filters: [ - { type: 'resource', op: 'include', value: 'user.profile' }, - { type: 'tag', op: 'include', value: 'admin' }, - ], - expected: [ - 'retrieve_user', - 'create_user_profile', - 'get_user_profile', - 'update_user_role_permissions', - 'get_organization_settings', - ], - }, - ]; - - tests.forEach((test) => { - it(`filters by ${test.name}`, () => { - const filtered = query(test.filters, endpoints); - expect(filtered.map((e) => e.tool.name)).toEqual(test.expected); - }); - }); -}); - -function endpoint({ - resource, - operation, - tags, - toolName, -}: { - resource: string; - operation: Metadata['operation']; - tags: string[]; - toolName: string; -}): Endpoint { - return { - metadata: { - resource, - operation, - tags, - }, - tool: { name: toolName, inputSchema: { type: 'object', properties: {} } }, - handler: jest.fn(), - }; -} diff --git a/src/resources/builds/builds.ts b/src/resources/builds/builds.ts index 2bd3d00d..e58ee61b 100644 --- a/src/resources/builds/builds.ts +++ b/src/resources/builds/builds.ts @@ -124,12 +124,16 @@ export namespace Build { node?: BuildsAPI.BuildTarget; + openapi?: BuildsAPI.BuildTarget; + php?: BuildsAPI.BuildTarget; python?: BuildsAPI.BuildTarget; ruby?: BuildsAPI.BuildTarget; + sql?: BuildsAPI.BuildTarget; + terraform?: BuildsAPI.BuildTarget; typescript?: BuildsAPI.BuildTarget; @@ -166,12 +170,36 @@ export namespace BuildTarget { } export interface Completed { + commit: Shared.Commit | null; + + /** + * deprecated + */ completed: Completed.Completed; + conclusion: + | 'error' + | 'warning' + | 'note' + | 'success' + | 'merge_conflict' + | 'upstream_merge_conflict' + | 'fatal' + | 'payment_required' + | 'cancelled' + | 'timed_out' + | 'noop' + | 'version_bump'; + + merge_conflict_pr: Completed.MergeConflictPr | null; + status: 'completed'; } export namespace Completed { + /** + * deprecated + */ export interface Completed { commit: Shared.Commit | null; @@ -207,6 +235,20 @@ export namespace BuildTarget { } } } + + export interface MergeConflictPr { + number: number; + + repo: MergeConflictPr.Repo; + } + + export namespace MergeConflictPr { + export interface Repo { + name: string; + + owner: string; + } + } } } @@ -226,12 +268,22 @@ export namespace CheckStep { } export interface Completed { + /** + * deprecated + */ completed: Completed.Completed; + conclusion: 'success' | 'failure' | 'skipped' | 'cancelled' | 'action_required' | 'neutral' | 'timed_out'; + status: 'completed'; + + url: string | null; } export namespace Completed { + /** + * deprecated + */ export interface Completed { conclusion: | 'success' @@ -314,12 +366,16 @@ export namespace BuildCreateParams { node?: string; + openapi?: string; + php?: string; python?: string; ruby?: string; + sql?: string; + terraform?: string; typescript?: string; diff --git a/src/resources/builds/target-outputs.ts b/src/resources/builds/target-outputs.ts index 5df86598..f7ca8dda 100644 --- a/src/resources/builds/target-outputs.ts +++ b/src/resources/builds/target-outputs.ts @@ -88,7 +88,9 @@ export interface TargetOutputRetrieveParams { | 'terraform' | 'cli' | 'php' - | 'csharp'; + | 'csharp' + | 'sql' + | 'openapi'; type: 'source' | 'dist' | 'wheel'; diff --git a/src/resources/orgs.ts b/src/resources/orgs.ts index 43b65133..4d9f9dc4 100644 --- a/src/resources/orgs.ts +++ b/src/resources/orgs.ts @@ -24,6 +24,8 @@ export class Orgs extends APIResource { export interface Org { display_name: string | null; + enable_ai_commit_messages: boolean; + object: 'org'; slug: string; diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 878dba97..3fb60095 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -45,4 +45,6 @@ export type Target = | 'terraform' | 'cli' | 'php' - | 'csharp'; + | 'csharp' + | 'sql' + | 'openapi'; diff --git a/src/version.ts b/src/version.ts index 807be518..48ca0ae0 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.1.0-alpha.19'; // x-release-please-version +export const VERSION = '0.1.0-alpha.20'; // x-release-please-version diff --git a/tests/api-resources/builds/builds.test.ts b/tests/api-resources/builds/builds.test.ts index cd2ba5ed..6bbaf78a 100644 --- a/tests/api-resources/builds/builds.test.ts +++ b/tests/api-resources/builds/builds.test.ts @@ -35,9 +35,11 @@ describe('resource builds', () => { java: 'java', kotlin: 'kotlin', node: 'node', + openapi: 'openapi', php: 'php', python: 'python', ruby: 'ruby', + sql: 'sql', terraform: 'terraform', typescript: 'typescript', },