From 64636793738166bd84b17f33aa18536364c860a4 Mon Sep 17 00:00:00 2001 From: agentHits <140916359+agentHits@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:56:33 +0300 Subject: [PATCH 1/2] feat: add application env upsert tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что: - Добавлен generated MCP tool `application-env-upsert` для `POST /application.env.upsert`. - Добавлена поддержка OpenAPI pattern-record схем в генераторе, чтобы MCP валидировал имена env-переменных и непустой `variables`. - Добавлено редактирование submitted env values в логах handler для нового tool и regression tests на безопасный routing. Зачем: - MCP-клиенты смогут безопасно отправлять только нужные application env variables без client-side read/replace полного env блока. - Это снижает риск удаления или раскрытия существующих секретов при автоматизации через MCP. Риски: - Endpoint пока зависит от Dokploy core PR #4581 и отсутствует в published OpenAPI. - Live Dokploy backend не вызывался. Проверки: - Команды и результаты: `corepack pnpm exec vitest run src/server.test.ts --reporter=verbose` passed, 1 file / 6 tests; `corepack pnpm run lint` passed with existing `src/utils/responseFormatter.ts` warning; `corepack pnpm run type-check` passed; `corepack pnpm run test` passed, 3 files / 27 tests; `corepack pnpm run build` passed; `git diff --check` passed. - Ограничения: live Dokploy backend and final core OpenAPI sync were not verified because core PR #4581 is still open. What: - Added the generated MCP `application-env-upsert` tool for `POST /application.env.upsert`. - Added OpenAPI pattern-record support to the generator so MCP validates env variable names and non-empty `variables`. - Added handler log redaction for submitted env values on the new tool and regression tests for safe routing. Why: - MCP clients can send only the requested application env variables without client-side full env read/replace. - This reduces the risk of deleting or exposing existing secrets during MCP automation. Risks: - The endpoint still depends on Dokploy core PR #4581 and is not present in the published OpenAPI yet. - Live Dokploy backend was not exercised. Checks: - Commands and results: `corepack pnpm exec vitest run src/server.test.ts --reporter=verbose` passed, 1 file / 6 tests; `corepack pnpm run lint` passed with existing `src/utils/responseFormatter.ts` warning; `corepack pnpm run type-check` passed; `corepack pnpm run test` passed, 3 files / 27 tests; `corepack pnpm run build` passed; `git diff --check` passed. - Limitations: live Dokploy backend and final core OpenAPI sync were not verified because core PR #4581 is still open. --- TOOLS.md | 5 +- scripts/generate-tools.ts | 78 ++++++++++++++- src/generated/openapi.json | 162 +++++++++++++++++++++++++++++++- src/generated/tools.ts | 14 ++- src/handler.ts | 19 +++- src/server.test.ts | 188 +++++++++++++++++++++++++++++++++++-- 6 files changed, 453 insertions(+), 13 deletions(-) diff --git a/TOOLS.md b/TOOLS.md index fa69db1..cc69c5d 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -2,14 +2,14 @@ > Auto-generated from the [Dokploy OpenAPI spec](https://docs.dokploy.com/openapi.json). Run `pnpm generate` to update. -- **Total Tools**: 524 +- **Total Tools**: 525 - **Categories**: 48 ## Categories - [admin](#admin) (1 tools) - [ai](#ai) (12 tools) -- [application](#application) (31 tools) +- [application](#application) (32 tools) - [auditLog](#auditLog) (1 tools) - [backup](#backup) (12 tools) - [bitbucket](#bitbucket) (7 tools) @@ -91,6 +91,7 @@ | `application-start` | POST | `applicationId` (string) | | `application-redeploy` | POST | `applicationId` (string), `title`?, `description`? | | `application-saveEnvironment` | POST | `applicationId` (string), `env` (string | null), `buildArgs` (string | null), `buildSecrets` (string | null), `createEnvFile` (boolean) | +| `application-env-upsert` | POST | `applicationId` (string), `variables` (object), `redeploy`?, `dryRun`?, `expectedRevision`? | | `application-saveBuildType` | POST | `applicationId` (string), `buildType` ("dockerfile" | "heroku_buildpacks" | "paketo_buildpacks" | "nixpacks" | "static" | "railpack"), `dockerfile` (string | null), `dockerContextPath` (string | null), `dockerBuildStage` (string | null), `herokuVersion` (string | null), `railpackVersion` (string | null), `publishDirectory`?, `isStaticSpa`? | | `application-saveGithubProvider` | POST | `applicationId` (string), `repository` (string | null), `branch` (string | null), `owner` (string | null), `buildPath` (string | null), `githubId` (string | null), `triggerType` ("push" | "tag"), `enableSubmodules`?, `watchPaths`? | | `application-saveGitlabProvider` | POST | `applicationId` (string), `gitlabBranch` (string | null), `gitlabBuildPath` (string | null), `gitlabOwner` (string | null), `gitlabRepository` (string | null), `gitlabId` (string | null), `gitlabProjectId` (number | null), `gitlabPathNamespace` (string | null), `enableSubmodules`?, `watchPaths`? | diff --git a/scripts/generate-tools.ts b/scripts/generate-tools.ts index 581f46c..63911d2 100644 --- a/scripts/generate-tools.ts +++ b/scripts/generate-tools.ts @@ -66,7 +66,7 @@ function formatTitle(operationId: string): string { function getZodSchema(op: OperationObject, method: string): string { if (method === "post" && op.requestBody?.content?.["application/json"]?.schema) { const schema = op.requestBody.content["application/json"].schema; - return jsonSchemaToZod(schema); + return customZodObjectSchema(schema) ?? jsonSchemaToZod(schema); } if (op.parameters && op.parameters.length > 0) { @@ -101,6 +101,82 @@ interface JsonSchemaObject { anyOf?: JsonSchemaObject[]; enum?: string[]; items?: JsonSchemaObject; + additionalProperties?: JsonSchemaObject | boolean; + propertyNames?: JsonSchemaObject; + pattern?: string; + minLength?: number; + maxLength?: number; + minProperties?: number; +} + +function zodStringSchema(schema: JsonSchemaObject): string { + let zod = "z.string()"; + if (schema.pattern) { + zod += `.regex(new RegExp(${JSON.stringify(schema.pattern)}))`; + } + if (typeof schema.minLength === "number") { + zod += `.min(${schema.minLength})`; + } + if (typeof schema.maxLength === "number") { + zod += `.max(${schema.maxLength})`; + } + return zod; +} + +function customZodRecordSchema(schema: JsonSchemaObject): string | null { + if ( + schema.type !== "object" || + typeof schema.additionalProperties !== "object" || + schema.additionalProperties === null || + schema.additionalProperties.type !== "string" || + schema.propertyNames?.pattern === undefined + ) { + return null; + } + + let zod = `z.record(${zodStringSchema(schema.propertyNames)}, ${zodStringSchema(schema.additionalProperties)})`; + + if (typeof schema.minProperties === "number" && schema.minProperties > 0) { + zod += `.refine((value) => Object.keys(value).length >= ${schema.minProperties}, "At least ${schema.minProperties} propert${ + schema.minProperties === 1 ? "y is" : "ies are" + } required")`; + } + + return zod; +} + +function customZodObjectSchema(schema: JsonSchema): string | null { + if ( + typeof schema !== "object" || + schema === null || + schema.type !== "object" || + !schema.properties + ) { + return null; + } + + const objectSchema = schema as JsonSchemaObject; + const requiredSet = new Set(objectSchema.required || []); + const propertyEntries = Object.entries(objectSchema.properties).map(([name, propSchema]) => ({ + name, + propSchema, + customSchema: customZodRecordSchema(propSchema), + })); + + if (!propertyEntries.some((entry) => entry.customSchema !== null)) { + return null; + } + + const entries = propertyEntries.map(({ name, propSchema, customSchema }) => { + const propertySchema = customSchema ?? jsonSchemaToZod(propSchema as JsonSchema); + return `${JSON.stringify(name)}: ${propertySchema}${requiredSet.has(name) ? "" : ".optional()"}`; + }); + + let zod = `z.object({ ${entries.join(", ")} })`; + if (objectSchema.additionalProperties === false) { + zod += ".strict()"; + } + return zod; } interface ToolMdEntry { diff --git a/src/generated/openapi.json b/src/generated/openapi.json index 9a33316..98ff25d 100644 --- a/src/generated/openapi.json +++ b/src/generated/openapi.json @@ -1087,6 +1087,166 @@ } } }, + "/application.env.upsert": { + "post": { + "operationId": "application-env-upsert", + "tags": [ + "application" + ], + "security": [ + { + "x-api-key": [] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "applicationId": { + "type": "string", + "minLength": 1 + }, + "variables": { + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "additionalProperties": { + "type": "string" + }, + "minProperties": 1 + }, + "redeploy": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "expectedRevision": { + "type": "string" + } + }, + "required": [ + "applicationId", + "variables" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "applicationId": { + "type": "string" + }, + "changed": { + "type": "boolean" + }, + "revision": { + "type": "string" + }, + "dryRun": { + "type": "boolean" + }, + "redeployed": { + "type": "boolean" + }, + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "action": { + "type": "string", + "enum": [ + "created", + "updated", + "unchanged" + ] + }, + "secret": { + "type": "boolean" + } + }, + "required": [ + "name", + "action", + "secret" + ], + "additionalProperties": false + } + } + }, + "required": [ + "applicationId", + "changed", + "revision", + "dryRun", + "redeployed", + "variables" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Invalid input data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error.BAD_REQUEST" + } + } + } + }, + "401": { + "description": "Authorization not provided", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error.UNAUTHORIZED" + } + } + } + }, + "403": { + "description": "Insufficient access", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error.FORBIDDEN" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR" + } + } + } + } + } + } + }, "/application.saveBuildType": { "post": { "operationId": "application-saveBuildType", @@ -57522,4 +57682,4 @@ "x-api-key": [] } ] -} \ No newline at end of file +} diff --git a/src/generated/tools.ts b/src/generated/tools.ts index a141bc0..71d5c29 100644 --- a/src/generated/tools.ts +++ b/src/generated/tools.ts @@ -1,5 +1,5 @@ // AUTO-GENERATED FILE — DO NOT EDIT MANUALLY -// Generated from openapi.json on 2026-04-25 +// Generated from openapi.json on 2026-07-02 // Run `pnpm generate` to regenerate import { z } from "zod"; @@ -114,6 +114,18 @@ export const generatedTools: ToolDefinition[] = [ ...{"idempotentHint":true,"openWorldHint":true}, }, }, + { + name: "application-env-upsert", + description: "POST /application.env.upsert", + tag: "application", + method: "POST", + path: "/application.env.upsert", + schema: z.object({ "applicationId": z.string().min(1), "variables": z.record(z.string().regex(new RegExp("^[A-Za-z_][A-Za-z0-9_]*$")), z.string()).refine((value) => Object.keys(value).length >= 1, "At least 1 property is required"), "redeploy": z.boolean().optional(), "dryRun": z.boolean().optional(), "expectedRevision": z.string().optional() }), + annotations: { + title: "Application Env Upsert", + ...{"idempotentHint":true,"openWorldHint":true}, + }, + }, { name: "application-saveBuildType", description: "POST /application.saveBuildType", diff --git a/src/handler.ts b/src/handler.ts index 0af976c..1f0758c 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -7,10 +7,27 @@ import { ResponseFormatter } from "./utils/responseFormatter.js"; const logger = createLogger("ToolHandler"); +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function getRedactFieldsForTool(tool: ToolDefinition, input: Record) { + if (tool.name !== "application-env-upsert" || !isRecord(input.variables)) { + return []; + } + + return Object.keys(input.variables); +} + export function createHandler(tool: ToolDefinition) { return async (input: Record) => { const { redactEnv, redactFields } = getClientConfig(); - const redact = (value: T): T => (redactEnv ? redactSensitive(value, redactFields) : value); + const toolRedactFields = getRedactFieldsForTool(tool, input); + const effectiveRedactFields = redactEnv + ? [...new Set([...redactFields, ...toolRedactFields])] + : toolRedactFields; + const redact = (value: T): T => + effectiveRedactFields.length > 0 ? redactSensitive(value, effectiveRedactFields) : value; try { logger.info(`Executing tool: ${tool.name}`, { input: redact(input) }); diff --git a/src/server.test.ts b/src/server.test.ts index 663dbad..58e6a7c 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -1,6 +1,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Mock apiClient before server.ts is imported — it calls getClientConfig() at // module level which requires DOKPLOY_URL/DOKPLOY_API_KEY env vars. @@ -10,15 +10,61 @@ vi.mock("./utils/apiClient.js", () => ({ clearAuthToken: vi.fn(), })); +const { default: apiClient } = await import("./utils/apiClient.js"); +const { generatedTools } = await import("./generated/tools.js"); const { createServer } = await import("./server.js"); describe("MCP server tools/list", () => { - async function getToolList() { + const originalDokployUrl = process.env.DOKPLOY_URL; + const originalDokployApiKey = process.env.DOKPLOY_API_KEY; + const originalDokployRedactEnv = process.env.DOKPLOY_REDACT_ENV; + const originalDokployRedactFields = process.env.DOKPLOY_REDACT_FIELDS; + + beforeEach(() => { + vi.clearAllMocks(); + process.env.DOKPLOY_URL = "https://dokploy.example"; + process.env.DOKPLOY_API_KEY = "test-api-key"; + process.env.DOKPLOY_REDACT_ENV = "false"; + delete process.env.DOKPLOY_REDACT_FIELDS; + }); + + afterEach(() => { + if (originalDokployUrl === undefined) { + delete process.env.DOKPLOY_URL; + } else { + process.env.DOKPLOY_URL = originalDokployUrl; + } + + if (originalDokployApiKey === undefined) { + delete process.env.DOKPLOY_API_KEY; + } else { + process.env.DOKPLOY_API_KEY = originalDokployApiKey; + } + + if (originalDokployRedactEnv === undefined) { + delete process.env.DOKPLOY_REDACT_ENV; + } else { + process.env.DOKPLOY_REDACT_ENV = originalDokployRedactEnv; + } + + if (originalDokployRedactFields === undefined) { + delete process.env.DOKPLOY_REDACT_FIELDS; + } else { + process.env.DOKPLOY_REDACT_FIELDS = originalDokployRedactFields; + } + }); + + async function createConnectedClient() { const server = createServer(); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); const client = new Client({ name: "test-client", version: "1.0.0" }); await client.connect(clientTransport); + return client; + } + + async function getToolList() { + const client = await createConnectedClient(); const { tools } = await client.listTools(); await client.close(); return tools; @@ -33,10 +79,9 @@ describe("MCP server tools/list", () => { const tools = await getToolList(); for (const tool of tools) { const schema = tool.inputSchema as Record; - expect( - schema.$schema, - `Tool "${tool.name}" is missing $schema or has wrong draft`, - ).toBe("https://json-schema.org/draft/2020-12/schema"); + expect(schema.$schema, `Tool "${tool.name}" is missing $schema or has wrong draft`).toBe( + "https://json-schema.org/draft/2020-12/schema", + ); } }); @@ -60,7 +105,10 @@ describe("MCP server tools/list", () => { for (const tool of tools) { const found = findNestedSchemaKeys(tool.inputSchema); - expect(found, `Tool "${tool.name}" has nested $schema keys at: ${found.join(", ")}`).toHaveLength(0); + expect( + found, + `Tool "${tool.name}" has nested $schema keys at: ${found.join(", ")}`, + ).toHaveLength(0); } }); @@ -75,4 +123,130 @@ describe("MCP server tools/list", () => { ).toBe("object"); } }); + + it("exposes application env upsert with partial-update inputs", async () => { + const tools = await getToolList(); + const tool = tools.find((candidate) => candidate.name === "application-env-upsert"); + + expect(tool).toBeDefined(); + expect(tool?.description).toBe("POST /application.env.upsert"); + + const schema = tool?.inputSchema as Record; + const properties = schema.properties as Record>; + + expect(schema.required).toEqual(["applicationId", "variables"]); + expect(properties.applicationId.type).toBe("string"); + expect(properties.variables.type).toBe("object"); + expect(properties.variables.additionalProperties).toMatchObject({ + type: "string", + }); + expect(properties.variables.propertyNames).toMatchObject({ + pattern: "^[A-Za-z_][A-Za-z0-9_]*$", + }); + expect(properties.redeploy.type).toBe("boolean"); + expect(properties.dryRun.type).toBe("boolean"); + expect(properties.expectedRevision.type).toBe("string"); + + const generatedTool = generatedTools.find( + (candidate) => candidate.name === "application-env-upsert", + ); + expect(generatedTool).toBeDefined(); + expect( + generatedTool?.schema.safeParse({ + applicationId: "app_1", + variables: { + REDIS_PASSWORD: "placeholder-secret-value", + }, + }).success, + ).toBe(true); + expect( + generatedTool?.schema.safeParse({ + applicationId: "app_1", + variables: {}, + }).success, + ).toBe(false); + expect( + generatedTool?.schema.safeParse({ + applicationId: "app_1", + variables: { + "1_BAD": "placeholder-secret-value", + }, + }).success, + ).toBe(false); + }); + + it("routes application env upsert without full environment replacement or raw value output", async () => { + vi.mocked(apiClient.post).mockResolvedValue({ + data: { + applicationId: "app_1", + changed: true, + revision: "env:next", + dryRun: true, + redeployed: false, + variables: [ + { + name: "REDIS_PASSWORD", + action: "updated", + secret: true, + }, + ], + }, + }); + + const client = await createConnectedClient(); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const result = await client.callTool({ + name: "application-env-upsert", + arguments: { + applicationId: "app_1", + variables: { + REDIS_PASSWORD: "placeholder-secret-value", + }, + dryRun: true, + redeploy: false, + expectedRevision: "env:current", + }, + }); + + expect(apiClient.get).not.toHaveBeenCalled(); + expect(apiClient.post).toHaveBeenCalledTimes(1); + expect(apiClient.post).toHaveBeenCalledWith("/application.env.upsert", { + applicationId: "app_1", + variables: { + REDIS_PASSWORD: "placeholder-secret-value", + }, + dryRun: true, + redeploy: false, + expectedRevision: "env:current", + }); + expect(apiClient.post).not.toHaveBeenCalledWith( + "/application.saveEnvironment", + expect.anything(), + ); + + const [, postBody] = vi.mocked(apiClient.post).mock.calls[0] as [ + string, + Record, + ]; + expect(postBody).not.toHaveProperty("env"); + expect(postBody).not.toHaveProperty("buildArgs"); + expect(postBody).not.toHaveProperty("buildSecrets"); + expect(postBody).not.toHaveProperty("createEnvFile"); + + const responseText = result.content + .map((item) => (item.type === "text" ? item.text : "")) + .join("\n"); + const logText = consoleError.mock.calls.map((call) => String(call[0])).join("\n"); + + expect(responseText).toContain('"applicationId": "app_1"'); + expect(responseText).toContain('"secret": true'); + expect(responseText).not.toContain("placeholder-secret-value"); + expect(logText).toContain('"REDIS_PASSWORD":"[REDACTED]"'); + expect(logText).not.toContain("placeholder-secret-value"); + } finally { + consoleError.mockRestore(); + await client.close(); + } + }); }); From 9796fc1ac6c7fe04b0082224bfe5d81055461c9e Mon Sep 17 00:00:00 2001 From: agentHits <140916359+agentHits@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:50:18 +0300 Subject: [PATCH 2/2] fix: call env upsert through slash route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что: - Перевел generated MCP tool `application-env-upsert` на `/application/env/upsert`, синхронизировал OpenAPI snapshot и обновил regression test. Зачем: - Core API support теперь публикует и обслуживает slash route, поэтому MCP PR должен отправлять запрос в тот route, который обслуживается рабочей core-реализацией. Риски: - Изменение касается только одного generated tool path; input schema, redaction guard и full-env replacement запрет не менялись. Проверки: - Команды и результаты: `corepack pnpm run generate` прошел и regenerated 525 tools; `corepack pnpm test` прошел, 3 файла и 27 тестов; `corepack pnpm run type-check` прошел; `corepack pnpm run lint` прошел с существующим warning в `src/utils/responseFormatter.ts`; `corepack pnpm run build` прошел; `git diff --check origin/feat/application-env-upsert...HEAD` прошел после amend. - Ограничения: live call against merged upstream Dokploy core не запускался, потому что core PR еще открыт. What: - Switched the generated MCP `application-env-upsert` tool to `/application/env/upsert`, synchronized the OpenAPI snapshot, and updated the regression test. Why: - Core API support now publishes and serves the slash route, so the MCP PR should call the route served by the working core implementation. Risks: - The change only affects one generated tool path; input schema, the redaction guard, and the no-full-env-replacement guarantee are unchanged. Checks: - Commands and results: `corepack pnpm run generate` passed and regenerated 525 tools; `corepack pnpm test` passed, 3 files and 27 tests; `corepack pnpm run type-check` passed; `corepack pnpm run lint` passed with the existing warning in `src/utils/responseFormatter.ts`; `corepack pnpm run build` passed; `git diff --check origin/feat/application-env-upsert...HEAD` passed after amend. - Limitations: live call against merged upstream Dokploy core was not run because the core PR is still open. --- src/generated/openapi.json | 2 +- src/generated/tools.ts | 6 +++--- src/server.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/generated/openapi.json b/src/generated/openapi.json index 98ff25d..202a578 100644 --- a/src/generated/openapi.json +++ b/src/generated/openapi.json @@ -1087,7 +1087,7 @@ } } }, - "/application.env.upsert": { + "/application/env/upsert": { "post": { "operationId": "application-env-upsert", "tags": [ diff --git a/src/generated/tools.ts b/src/generated/tools.ts index 71d5c29..f560eda 100644 --- a/src/generated/tools.ts +++ b/src/generated/tools.ts @@ -1,5 +1,5 @@ // AUTO-GENERATED FILE — DO NOT EDIT MANUALLY -// Generated from openapi.json on 2026-07-02 +// Generated from openapi.json on 2026-07-06 // Run `pnpm generate` to regenerate import { z } from "zod"; @@ -116,10 +116,10 @@ export const generatedTools: ToolDefinition[] = [ }, { name: "application-env-upsert", - description: "POST /application.env.upsert", + description: "POST /application/env/upsert", tag: "application", method: "POST", - path: "/application.env.upsert", + path: "/application/env/upsert", schema: z.object({ "applicationId": z.string().min(1), "variables": z.record(z.string().regex(new RegExp("^[A-Za-z_][A-Za-z0-9_]*$")), z.string()).refine((value) => Object.keys(value).length >= 1, "At least 1 property is required"), "redeploy": z.boolean().optional(), "dryRun": z.boolean().optional(), "expectedRevision": z.string().optional() }), annotations: { title: "Application Env Upsert", diff --git a/src/server.test.ts b/src/server.test.ts index 58e6a7c..17eb05c 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -129,7 +129,7 @@ describe("MCP server tools/list", () => { const tool = tools.find((candidate) => candidate.name === "application-env-upsert"); expect(tool).toBeDefined(); - expect(tool?.description).toBe("POST /application.env.upsert"); + expect(tool?.description).toBe("POST /application/env/upsert"); const schema = tool?.inputSchema as Record; const properties = schema.properties as Record>; @@ -211,7 +211,7 @@ describe("MCP server tools/list", () => { expect(apiClient.get).not.toHaveBeenCalled(); expect(apiClient.post).toHaveBeenCalledTimes(1); - expect(apiClient.post).toHaveBeenCalledWith("/application.env.upsert", { + expect(apiClient.post).toHaveBeenCalledWith("/application/env/upsert", { applicationId: "app_1", variables: { REDIS_PASSWORD: "placeholder-secret-value",