diff --git a/packages/mcp-server/.dockerignore b/.dockerignore similarity index 90% rename from packages/mcp-server/.dockerignore rename to .dockerignore index 1850726..12ff1e6 100644 --- a/packages/mcp-server/.dockerignore +++ b/.dockerignore @@ -5,8 +5,6 @@ node_modules/ # Build outputs dist/ **/dist/ -build/ -**/build/ # Git .git/ @@ -28,11 +26,6 @@ build/ .DS_Store Thumbs.db -# Documentation -*.md -docs/ -LICENSE - # Testing test/ tests/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 19ee807..4a403c7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.0" + ".": "0.19.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7e0e9..05b1bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.19.1 (2026-02-14) + +Full Changelog: [v0.19.0...v0.19.1](https://github.com/ArkHQ-io/ark-nodejs/compare/v0.19.0...v0.19.1) + +### Chores + +* **internal:** allow basic filtering of methods allowed for MCP code mode ([2a08f18](https://github.com/ArkHQ-io/ark-nodejs/commit/2a08f182832a256142ddf2d829710a6f66149d47)) +* **internal:** always generate MCP server dockerfiles and upgrade associated dependencies ([037403e](https://github.com/ArkHQ-io/ark-nodejs/commit/037403eac179b8c1aaa756746276de7c4de38f04)) +* **internal:** avoid type checking errors with ts-reset ([9496839](https://github.com/ArkHQ-io/ark-nodejs/commit/9496839d6222a21c3c9aa676282cfa8e04d6edaf)) +* **internal:** improve layout of generated MCP server files ([20ba475](https://github.com/ArkHQ-io/ark-nodejs/commit/20ba4751619d64194cafc11340b29e0eb0cf830e)) +* **internal:** improve reliability of MCP servers when using local code mode execution ([00be033](https://github.com/ArkHQ-io/ark-nodejs/commit/00be033fe6e3341ef30fa2aebff9f343410ec1ee)) +* **mcp:** forward STAINLESS_API_KEY to docs search endpoint ([6315d60](https://github.com/ArkHQ-io/ark-nodejs/commit/6315d6035354116ec682d5436b31c196070c4574)) + ## 0.19.0 (2026-02-07) Full Changelog: [v0.18.0...v0.19.0](https://github.com/ArkHQ-io/ark-nodejs/compare/v0.18.0...v0.19.0) diff --git a/package.json b/package.json index 582550a..2dc7545 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ark-email", - "version": "0.19.0", + "version": "0.19.1", "description": "The official TypeScript library for the Ark API", "author": "Ark ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 7246d99..9704fa7 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -22,10 +22,11 @@ # enables interactive mode, allowing the container to communicate over stdin/stdout. # Build stage -FROM node:20-alpine AS builder +FROM node:24-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 @@ -35,12 +36,15 @@ WORKDIR /build # Copy entire repository COPY . . +# Set CI environment variable so pnpm install runs without prompts +ENV CI=true + # Install all dependencies and build everything RUN pnpm install --frozen-lockfile && \ pnpm build # Production stage -FROM node:20-alpine +FROM node:24-alpine # Add non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 @@ -48,12 +52,8 @@ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 # 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 -COPY --from=builder /build/packages/mcp-server/node_modules ./packages/mcp-server/node_modules - -# Copy node_modules from root (pnpm hoists dependencies here) -COPY --from=builder /build/node_modules ./node_modules +# Copy the build results, preserving directory structure +COPY --from=builder /build . # Copy the built ark-email into node_modules COPY --from=builder /build/dist ./node_modules/ark-email diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 96a4202..f8cc58e 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "ark-email-mcp", - "version": "0.19.0", + "version": "0.19.1", "description": "The official MCP Server for the Ark API", "author": "Ark ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/auth.ts similarity index 100% rename from packages/mcp-server/src/headers.ts rename to packages/mcp-server/src/auth.ts diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 528f5cf..d352e3b 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -2,8 +2,9 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { readEnv, requireValue } from './server'; +import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { SdkMethod } from './methods'; import { Ark } from 'ark-email'; const prompt = `Runs JavaScript code to interact with the Ark API. @@ -42,7 +43,7 @@ Variables will not persist between calls, so make sure to return or log any data * * @param endpoints - The endpoints to include in the list. */ -export function codeTool(): McpTool { +export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -66,6 +67,24 @@ export function codeTool(): McpTool { const code = args.code as string; const intent = args.intent as string | undefined; + // Do very basic blocking of code that includes forbidden method names. + // + // WARNING: This is not secure against obfuscation and other evasion methods. If + // stronger security blocks are required, then these should be enforced in the downstream + // API (e.g., by having users call the MCP server with API keys with limited permissions). + if (params.blockedMethods) { + const blockedMatches = params.blockedMethods.filter((method) => + code.includes(method.fullyQualifiedName), + ); + if (blockedMatches.length > 0) { + return asErrorResult( + `The following methods have been blocked by the MCP server and cannot be used in code execution: ${blockedMatches + .map((m) => m.fullyQualifiedName) + .join(', ')}`, + ); + } + } + // this is not required, but passing a Stainless API key for the matching project_name // will allow you to run code-mode queries against non-published versions of your SDK. const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 8a8f667..64d0de4 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; +import { readEnv } from './util'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -45,7 +46,12 @@ const docsSearchURL = export const handler = async (_: unknown, args: Record | undefined) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const result = await fetch(`${docsSearchURL}?${query}`); + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + const result = await fetch(`${docsSearchURL}?${query}`, { + headers: { + ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + }, + }); if (!result.ok) { throw new Error( diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index b203136..4588ff6 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,19 +2,22 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { ClientOptions } from 'ark-email'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; +import { parseAuthHeaders } from './auth'; import { McpOptions } from './options'; -import { ClientOptions, initMcpServer, newMcpServer } from './server'; -import { parseAuthHeaders } from './headers'; +import { initMcpServer, newMcpServer } from './server'; const newServer = async ({ clientOptions, + mcpOptions, req, res, }: { clientOptions: ClientOptions; + mcpOptions: McpOptions; req: express.Request; res: express.Response; }): Promise => { @@ -24,6 +27,7 @@ const newServer = async ({ const authOptions = parseAuthHeaders(req, false); await initMcpServer({ server: server, + mcpOptions: mcpOptions, clientOptions: { ...clientOptions, ...authOptions, diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index d75968e..003a765 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -18,7 +18,7 @@ async function main() { switch (options.transport) { case 'stdio': - await launchStdioServer(); + await launchStdioServer(options); break; case 'http': await launchStreamableHTTPServer({ diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts new file mode 100644 index 0000000..684fd87 --- /dev/null +++ b/packages/mcp-server/src/methods.ts @@ -0,0 +1,434 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { McpOptions } from './options'; + +export type SdkMethod = { + clientCallName: string; + fullyQualifiedName: string; + httpMethod?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'query'; + httpPath?: string; +}; + +export const sdkMethods: SdkMethod[] = [ + { + clientCallName: 'client.emails.retrieve', + fullyQualifiedName: 'emails.retrieve', + httpMethod: 'get', + httpPath: '/emails/{emailId}', + }, + { + clientCallName: 'client.emails.list', + fullyQualifiedName: 'emails.list', + httpMethod: 'get', + httpPath: '/emails', + }, + { + clientCallName: 'client.emails.retrieveDeliveries', + fullyQualifiedName: 'emails.retrieveDeliveries', + httpMethod: 'get', + httpPath: '/emails/{emailId}/deliveries', + }, + { + clientCallName: 'client.emails.retry', + fullyQualifiedName: 'emails.retry', + httpMethod: 'post', + httpPath: '/emails/{emailId}/retry', + }, + { + clientCallName: 'client.emails.send', + fullyQualifiedName: 'emails.send', + httpMethod: 'post', + httpPath: '/emails', + }, + { + clientCallName: 'client.emails.sendBatch', + fullyQualifiedName: 'emails.sendBatch', + httpMethod: 'post', + httpPath: '/emails/batch', + }, + { + clientCallName: 'client.emails.sendRaw', + fullyQualifiedName: 'emails.sendRaw', + httpMethod: 'post', + httpPath: '/emails/raw', + }, + { + clientCallName: 'client.logs.retrieve', + fullyQualifiedName: 'logs.retrieve', + httpMethod: 'get', + httpPath: '/logs/{requestId}', + }, + { + clientCallName: 'client.logs.list', + fullyQualifiedName: 'logs.list', + httpMethod: 'get', + httpPath: '/logs', + }, + { + clientCallName: 'client.usage.retrieve', + fullyQualifiedName: 'usage.retrieve', + httpMethod: 'get', + httpPath: '/usage', + }, + { + clientCallName: 'client.usage.export', + fullyQualifiedName: 'usage.export', + httpMethod: 'get', + httpPath: '/usage/export', + }, + { + clientCallName: 'client.usage.listTenants', + fullyQualifiedName: 'usage.listTenants', + httpMethod: 'get', + httpPath: '/usage/tenants', + }, + { + clientCallName: 'client.limits.retrieve', + fullyQualifiedName: 'limits.retrieve', + httpMethod: 'get', + httpPath: '/limits', + }, + { + clientCallName: 'client.tenants.create', + fullyQualifiedName: 'tenants.create', + httpMethod: 'post', + httpPath: '/tenants', + }, + { + clientCallName: 'client.tenants.retrieve', + fullyQualifiedName: 'tenants.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}', + }, + { + clientCallName: 'client.tenants.update', + fullyQualifiedName: 'tenants.update', + httpMethod: 'patch', + httpPath: '/tenants/{tenantId}', + }, + { + clientCallName: 'client.tenants.list', + fullyQualifiedName: 'tenants.list', + httpMethod: 'get', + httpPath: '/tenants', + }, + { + clientCallName: 'client.tenants.delete', + fullyQualifiedName: 'tenants.delete', + httpMethod: 'delete', + httpPath: '/tenants/{tenantId}', + }, + { + clientCallName: 'client.tenants.credentials.create', + fullyQualifiedName: 'tenants.credentials.create', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/credentials', + }, + { + clientCallName: 'client.tenants.credentials.retrieve', + fullyQualifiedName: 'tenants.credentials.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/credentials/{credentialId}', + }, + { + clientCallName: 'client.tenants.credentials.update', + fullyQualifiedName: 'tenants.credentials.update', + httpMethod: 'patch', + httpPath: '/tenants/{tenantId}/credentials/{credentialId}', + }, + { + clientCallName: 'client.tenants.credentials.list', + fullyQualifiedName: 'tenants.credentials.list', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/credentials', + }, + { + clientCallName: 'client.tenants.credentials.delete', + fullyQualifiedName: 'tenants.credentials.delete', + httpMethod: 'delete', + httpPath: '/tenants/{tenantId}/credentials/{credentialId}', + }, + { + clientCallName: 'client.tenants.domains.create', + fullyQualifiedName: 'tenants.domains.create', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/domains', + }, + { + clientCallName: 'client.tenants.domains.retrieve', + fullyQualifiedName: 'tenants.domains.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/domains/{domainId}', + }, + { + clientCallName: 'client.tenants.domains.list', + fullyQualifiedName: 'tenants.domains.list', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/domains', + }, + { + clientCallName: 'client.tenants.domains.delete', + fullyQualifiedName: 'tenants.domains.delete', + httpMethod: 'delete', + httpPath: '/tenants/{tenantId}/domains/{domainId}', + }, + { + clientCallName: 'client.tenants.domains.verify', + fullyQualifiedName: 'tenants.domains.verify', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/domains/{domainId}/verify', + }, + { + clientCallName: 'client.tenants.suppressions.create', + fullyQualifiedName: 'tenants.suppressions.create', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/suppressions', + }, + { + clientCallName: 'client.tenants.suppressions.retrieve', + fullyQualifiedName: 'tenants.suppressions.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/suppressions/{email}', + }, + { + clientCallName: 'client.tenants.suppressions.list', + fullyQualifiedName: 'tenants.suppressions.list', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/suppressions', + }, + { + clientCallName: 'client.tenants.suppressions.delete', + fullyQualifiedName: 'tenants.suppressions.delete', + httpMethod: 'delete', + httpPath: '/tenants/{tenantId}/suppressions/{email}', + }, + { + clientCallName: 'client.tenants.webhooks.create', + fullyQualifiedName: 'tenants.webhooks.create', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/webhooks', + }, + { + clientCallName: 'client.tenants.webhooks.retrieve', + fullyQualifiedName: 'tenants.webhooks.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}', + }, + { + clientCallName: 'client.tenants.webhooks.update', + fullyQualifiedName: 'tenants.webhooks.update', + httpMethod: 'patch', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}', + }, + { + clientCallName: 'client.tenants.webhooks.list', + fullyQualifiedName: 'tenants.webhooks.list', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/webhooks', + }, + { + clientCallName: 'client.tenants.webhooks.delete', + fullyQualifiedName: 'tenants.webhooks.delete', + httpMethod: 'delete', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}', + }, + { + clientCallName: 'client.tenants.webhooks.listDeliveries', + fullyQualifiedName: 'tenants.webhooks.listDeliveries', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}/deliveries', + }, + { + clientCallName: 'client.tenants.webhooks.replayDelivery', + fullyQualifiedName: 'tenants.webhooks.replayDelivery', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{deliveryId}/replay', + }, + { + clientCallName: 'client.tenants.webhooks.retrieveDelivery', + fullyQualifiedName: 'tenants.webhooks.retrieveDelivery', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{deliveryId}', + }, + { + clientCallName: 'client.tenants.webhooks.test', + fullyQualifiedName: 'tenants.webhooks.test', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/webhooks/{webhookId}/test', + }, + { + clientCallName: 'client.tenants.tracking.create', + fullyQualifiedName: 'tenants.tracking.create', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/tracking', + }, + { + clientCallName: 'client.tenants.tracking.retrieve', + fullyQualifiedName: 'tenants.tracking.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/tracking/{trackingId}', + }, + { + clientCallName: 'client.tenants.tracking.update', + fullyQualifiedName: 'tenants.tracking.update', + httpMethod: 'patch', + httpPath: '/tenants/{tenantId}/tracking/{trackingId}', + }, + { + clientCallName: 'client.tenants.tracking.list', + fullyQualifiedName: 'tenants.tracking.list', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/tracking', + }, + { + clientCallName: 'client.tenants.tracking.delete', + fullyQualifiedName: 'tenants.tracking.delete', + httpMethod: 'delete', + httpPath: '/tenants/{tenantId}/tracking/{trackingId}', + }, + { + clientCallName: 'client.tenants.tracking.verify', + fullyQualifiedName: 'tenants.tracking.verify', + httpMethod: 'post', + httpPath: '/tenants/{tenantId}/tracking/{trackingId}/verify', + }, + { + clientCallName: 'client.tenants.usage.retrieve', + fullyQualifiedName: 'tenants.usage.retrieve', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/usage', + }, + { + clientCallName: 'client.tenants.usage.retrieveTimeseries', + fullyQualifiedName: 'tenants.usage.retrieveTimeseries', + httpMethod: 'get', + httpPath: '/tenants/{tenantId}/usage/timeseries', + }, + { + clientCallName: 'client.platform.webhooks.create', + fullyQualifiedName: 'platform.webhooks.create', + httpMethod: 'post', + httpPath: '/platform/webhooks', + }, + { + clientCallName: 'client.platform.webhooks.retrieve', + fullyQualifiedName: 'platform.webhooks.retrieve', + httpMethod: 'get', + httpPath: '/platform/webhooks/{webhookId}', + }, + { + clientCallName: 'client.platform.webhooks.update', + fullyQualifiedName: 'platform.webhooks.update', + httpMethod: 'patch', + httpPath: '/platform/webhooks/{webhookId}', + }, + { + clientCallName: 'client.platform.webhooks.list', + fullyQualifiedName: 'platform.webhooks.list', + httpMethod: 'get', + httpPath: '/platform/webhooks', + }, + { + clientCallName: 'client.platform.webhooks.delete', + fullyQualifiedName: 'platform.webhooks.delete', + httpMethod: 'delete', + httpPath: '/platform/webhooks/{webhookId}', + }, + { + clientCallName: 'client.platform.webhooks.listDeliveries', + fullyQualifiedName: 'platform.webhooks.listDeliveries', + httpMethod: 'get', + httpPath: '/platform/webhooks/deliveries', + }, + { + clientCallName: 'client.platform.webhooks.replayDelivery', + fullyQualifiedName: 'platform.webhooks.replayDelivery', + httpMethod: 'post', + httpPath: '/platform/webhooks/deliveries/{deliveryId}/replay', + }, + { + clientCallName: 'client.platform.webhooks.retrieveDelivery', + fullyQualifiedName: 'platform.webhooks.retrieveDelivery', + httpMethod: 'get', + httpPath: '/platform/webhooks/deliveries/{deliveryId}', + }, + { + clientCallName: 'client.platform.webhooks.test', + fullyQualifiedName: 'platform.webhooks.test', + httpMethod: 'post', + httpPath: '/platform/webhooks/{webhookId}/test', + }, +]; + +function allowedMethodsForCodeTool(options: McpOptions | undefined): SdkMethod[] | undefined { + if (!options) { + return undefined; + } + + let allowedMethods: SdkMethod[]; + + if (options.codeAllowHttpGets || options.codeAllowedMethods) { + // Start with nothing allowed and then add into it from options + let allowedMethodsSet = new Set(); + + if (options.codeAllowHttpGets) { + // Add all methods that map to an HTTP GET + sdkMethods + .filter((method) => method.httpMethod === 'get') + .forEach((method) => allowedMethodsSet.add(method)); + } + + if (options.codeAllowedMethods) { + // Add all methods that match any of the allowed regexps + const allowedRegexps = options.codeAllowedMethods.map((pattern) => { + try { + return new RegExp(pattern); + } catch (e) { + throw new Error( + `Invalid regex pattern for allowed method: "${pattern}": ${e instanceof Error ? e.message : e}`, + ); + } + }); + + sdkMethods + .filter((method) => allowedRegexps.some((regexp) => regexp.test(method.fullyQualifiedName))) + .forEach((method) => allowedMethodsSet.add(method)); + } + + allowedMethods = Array.from(allowedMethodsSet); + } else { + // Start with everything allowed + allowedMethods = [...sdkMethods]; + } + + if (options.codeBlockedMethods) { + // Filter down based on blocked regexps + const blockedRegexps = options.codeBlockedMethods.map((pattern) => { + try { + return new RegExp(pattern); + } catch (e) { + throw new Error( + `Invalid regex pattern for blocked method: "${pattern}": ${e instanceof Error ? e.message : e}`, + ); + } + }); + + allowedMethods = allowedMethods.filter( + (method) => !blockedRegexps.some((regexp) => regexp.test(method.fullyQualifiedName)), + ); + } + + return allowedMethods; +} + +export function blockedMethodsForCodeTool(options: McpOptions | undefined): SdkMethod[] | undefined { + const allowedMethods = allowedMethodsForCodeTool(options); + if (!allowedMethods) { + return undefined; + } + + const allowedSet = new Set(allowedMethods.map((method) => method.fullyQualifiedName)); + + // Return any methods that are not explicitly allowed + return sdkMethods.filter((method) => !allowedSet.has(method.fullyQualifiedName)); +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 7438083..cfde21d 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -12,10 +14,30 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + codeAllowHttpGets?: boolean | undefined; + codeAllowedMethods?: string[] | undefined; + codeBlockedMethods?: string[] | undefined; }; export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) + .option('code-allow-http-gets', { + type: 'boolean', + description: + 'Allow all code tool methods that map to HTTP GET operations. If all code-allow-* flags are unset, then everything is allowed.', + }) + .option('code-allowed-methods', { + type: 'string', + array: true, + description: + 'Methods to explicitly allow for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', + }) + .option('code-blocked-methods', { + type: 'string', + array: true, + description: + 'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', + }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) .option('no-tools', { type: 'string', @@ -59,6 +81,9 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, + codeAllowHttpGets: argv.codeAllowHttpGets, + codeAllowedMethods: argv.codeAllowedMethods, + codeBlockedMethods: argv.codeBlockedMethods, transport, port: argv.port, socket: argv.socket, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 2e4b8fb..b043afa 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -12,10 +12,9 @@ import Ark from 'ark-email'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; +import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpTool } from './types'; - -export { McpOptions } from './options'; -export { ClientOptions } from 'ark-email'; +import { readEnv } from './util'; async function getInstructions() { // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. @@ -57,7 +56,7 @@ export const newMcpServer = async () => new McpServer( { name: 'ark_email_api', - version: '0.19.0', + version: '0.19.1', }, { instructions: await getInstructions(), @@ -147,7 +146,11 @@ export async function initMcpServer(params: { * Selects the tools to include in the MCP Server based on the provided options. */ export function selectTools(options?: McpOptions): McpTool[] { - const includedTools = [codeTool()]; + const includedTools = [ + codeTool({ + blockedMethods: blockedMethodsForCodeTool(options), + }), + ]; if (options?.includeDocsTools ?? true) { includedTools.push(docsSearchTool); } @@ -164,27 +167,3 @@ export async function executeHandler( ) { return await handler(client, args || {}); } - -export const readEnv = (env: string): string | undefined => { - if (typeof (globalThis as any).process !== 'undefined') { - return (globalThis as any).process.env?.[env]?.trim(); - } else if (typeof (globalThis as any).Deno !== 'undefined') { - return (globalThis as any).Deno.env?.get?.(env)?.trim(); - } - return; -}; - -export const readEnvOrError = (env: string): string => { - let envValue = readEnv(env); - if (envValue === undefined) { - throw new Error(`Environment variable ${env} is not set`); - } - return envValue; -}; - -export const requireValue = (value: T | undefined, description: string): T => { - if (value === undefined) { - throw new Error(`Missing required value: ${description}`); - } - return value; -}; diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index 47aeb0c..57b9912 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,10 +1,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; -export const launchStdioServer = async () => { +export const launchStdioServer = async (mcpOptions: McpOptions) => { const server = await newMcpServer(); - await initMcpServer({ server }); + await initMcpServer({ server, mcpOptions }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/src/util.ts b/packages/mcp-server/src/util.ts new file mode 100644 index 0000000..40ed550 --- /dev/null +++ b/packages/mcp-server/src/util.ts @@ -0,0 +1,25 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const readEnv = (env: string): string | undefined => { + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim(); + } else if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim(); + } + return; +}; + +export const readEnvOrError = (env: string): string => { + let envValue = readEnv(env); + if (envValue === undefined) { + throw new Error(`Environment variable ${env} is not set`); + } + return envValue; +}; + +export const requireValue = (value: T | undefined, description: string): T => { + if (value === undefined) { + throw new Error(`Missing required value: ${description}`); + } + return value; +}; diff --git a/src/client.ts b/src/client.ts index 2361bfd..e71b2b3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -509,7 +509,7 @@ export class Ark { loggerFor(this).info(`${responseInfo} - ${retryMessage}`); const errText = await response.text().catch((err: any) => castToError(err).message); - const errJSON = safeJSON(errText); + const errJSON = safeJSON(errText) as any; const errMessage = errJSON ? undefined : errText; loggerFor(this).debug( diff --git a/src/version.ts b/src/version.ts index 02c8084..f3ae0bb 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.19.0'; // x-release-please-version +export const VERSION = '0.19.1'; // x-release-please-version