feat: Komodo MCP server — 7 tools, full API coverage
- komodo_list, komodo_get, komodo_create, komodo_update, komodo_delete, komodo_execute, komodo_logs with resource_type routing - JWT auth with token caching and auto-refresh on 401 - HTTP/SSE transport on port 9800 - TypeScript, @modelcontextprotocol/sdk, zod schemas - README.md with setup instructions - references/api.md with full endpoint mapping
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ResourceType, CREATE_REQUEST_MAP } from "../types.js";
|
||||
|
||||
export const createInputSchema = {
|
||||
resource_type: ResourceType.describe(
|
||||
"Resource type to create (stack, build, server, procedure, deployment, alerter, image_registry_account, sync_resource, tag)",
|
||||
),
|
||||
params: z
|
||||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
.describe(
|
||||
"Resource-specific parameters passed directly to the Komodo Create* endpoint",
|
||||
),
|
||||
};
|
||||
|
||||
export async function handleCreate(
|
||||
args: {
|
||||
resource_type: z.infer<typeof ResourceType>;
|
||||
params?: Record<string, unknown>;
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, params } = args;
|
||||
|
||||
const requestName = CREATE_REQUEST_MAP[resource_type];
|
||||
if (!requestName) {
|
||||
throw new Error(
|
||||
`No CREATE endpoint available for resource type: ${resource_type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await client.rpc("write", requestName, params ?? {});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ResourceType, DELETE_REQUEST_MAP } from "../types.js";
|
||||
|
||||
export const deleteInputSchema = {
|
||||
resource_type: ResourceType.describe(
|
||||
"Resource type to delete (stack, build, server, procedure, deployment, alerter, sync_resource, tag)",
|
||||
),
|
||||
id: z.string().describe("Resource ID or name to delete"),
|
||||
};
|
||||
|
||||
export async function handleDelete(
|
||||
args: { resource_type: z.infer<typeof ResourceType>; id: string },
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, id } = args;
|
||||
|
||||
const requestName = DELETE_REQUEST_MAP[resource_type];
|
||||
if (!requestName) {
|
||||
throw new Error(
|
||||
`No DELETE endpoint available for resource type: ${resource_type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await client.rpc("write", requestName, { id });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ExecuteOperation, EXECUTE_REQUEST_MAP } from "../types.js";
|
||||
|
||||
export const executeInputSchema = {
|
||||
operation: ExecuteOperation.describe(
|
||||
"Execute operation (run_build, deploy_stack, deploy_stack_service, run_procedure, run_procedure_stage, run_deployment_action, run_deployment_sync, run_resource_sync, run_stack_refresh_cache, run_stack_refresh_content, run_stack_pull, run_stack_pull_deployment, run_stack_stop, run_stack_restart, run_stack_pause, run_stack_unpause, run_stack_remove_orphan_containers, run_stack_toggle_service_dependencies, run_stack_auto_update, run_stack_commit, run_server_refresh, run_server_refresh_containers, run_server_update_periphery, run_server_prune_images, run_server_prune_containers, run_server_prune_networks, run_server_stats, run_server_run_command, run_server_scripts, run_server_copy, run_server_move, run_deployment_execute, run_deployment_redeploy, run_deployment_destroy, run_deployment_stop, run_deployment_logs, run_sync_deployment, get_mcp_token)",
|
||||
),
|
||||
id: z.string().optional().describe("Resource ID or name for the operation"),
|
||||
params: z
|
||||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
.describe("Additional parameters for the operation"),
|
||||
};
|
||||
|
||||
export async function handleExecute(
|
||||
args: {
|
||||
operation: z.infer<typeof ExecuteOperation>;
|
||||
id?: string;
|
||||
params?: Record<string, unknown>;
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { operation, id, params } = args;
|
||||
|
||||
const mapping = EXECUTE_REQUEST_MAP[operation];
|
||||
if (!mapping) {
|
||||
throw new Error(`Unknown execute operation: ${operation}`);
|
||||
}
|
||||
|
||||
const requestParams: Record<string, unknown> = { ...params };
|
||||
if (id) requestParams.id = id;
|
||||
|
||||
const result = await client.rpc(mapping.route, mapping.name, requestParams);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ResourceType, GET_REQUEST_MAP } from "../types.js";
|
||||
|
||||
export const getInputSchema = {
|
||||
resource_type: ResourceType.describe(
|
||||
"Resource type to get (stack, build, server, procedure, deployment, alerter, image_registry_account, sync_resource, user, tag, execution, access_request)",
|
||||
),
|
||||
id: z.string().describe("Resource ID or name"),
|
||||
};
|
||||
|
||||
export async function handleGet(
|
||||
args: { resource_type: z.infer<typeof ResourceType>; id: string },
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, id } = args;
|
||||
|
||||
const requestName = GET_REQUEST_MAP[resource_type];
|
||||
if (!requestName) {
|
||||
throw new Error(
|
||||
`No GET endpoint available for resource type: ${resource_type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await client.rpc("read", requestName, { id });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import {
|
||||
ResourceType,
|
||||
LIST_REQUEST_MAP,
|
||||
SEARCH_REQUEST_MAP,
|
||||
} from "../types.js";
|
||||
|
||||
export const listInputSchema = {
|
||||
resource_type: ResourceType.describe(
|
||||
"Resource type to list (stack, build, server, procedure, deployment, alerter, image_registry_account, sync_resource, user, tag, execution, access_request)",
|
||||
),
|
||||
search: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional search/filter query — uses the Search* endpoint instead of List*"),
|
||||
project: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter by project name or ID"),
|
||||
};
|
||||
|
||||
export async function handleList(
|
||||
args: { resource_type: z.infer<typeof ResourceType>; search?: string; project?: string },
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, search, project } = args;
|
||||
|
||||
const params: Record<string, unknown> = {};
|
||||
if (search) params.filter = search;
|
||||
if (project) params.project = project;
|
||||
|
||||
let route: "read" | "write" | "execute" = "read";
|
||||
let requestName: string;
|
||||
|
||||
if (search && SEARCH_REQUEST_MAP[resource_type]) {
|
||||
requestName = SEARCH_REQUEST_MAP[resource_type];
|
||||
} else {
|
||||
requestName = LIST_REQUEST_MAP[resource_type];
|
||||
}
|
||||
|
||||
const result = await client.rpc(route, requestName, params);
|
||||
|
||||
// Komodo list endpoints return bare arrays
|
||||
const items = Array.isArray(result) ? result : [result];
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(items, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
|
||||
export const logsInputSchema = {
|
||||
resource_type: z
|
||||
.enum(["deployment", "stack"])
|
||||
.describe("Resource type to get logs for"),
|
||||
id: z.string().describe("Resource ID or name"),
|
||||
tail: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of recent log lines to return"),
|
||||
};
|
||||
|
||||
export async function handleLogs(
|
||||
args: {
|
||||
resource_type: "deployment" | "stack";
|
||||
id: string;
|
||||
tail?: number;
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, id, tail } = args;
|
||||
|
||||
const params: Record<string, unknown> = { id };
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
|
||||
let result: unknown;
|
||||
|
||||
if (resource_type === "deployment") {
|
||||
result = await client.rpc("read", "GetDeploymentLogs", params);
|
||||
} else {
|
||||
// stack — GetStack returns full stack state including logs
|
||||
result = await client.rpc("read", "GetStack", { id });
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
typeof result === "string"
|
||||
? result
|
||||
: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ResourceType, UPDATE_REQUEST_MAP } from "../types.js";
|
||||
|
||||
export const updateInputSchema = {
|
||||
resource_type: ResourceType.describe(
|
||||
"Resource type to update (stack, build, server, procedure, deployment, alerter, image_registry_account, sync_resource, user, tag)",
|
||||
),
|
||||
id: z.string().describe("Resource ID or name to update"),
|
||||
params: z
|
||||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
.describe(
|
||||
"Update fields passed directly to the Komodo Update* endpoint",
|
||||
),
|
||||
};
|
||||
|
||||
export async function handleUpdate(
|
||||
args: {
|
||||
resource_type: z.infer<typeof ResourceType>;
|
||||
id: string;
|
||||
params?: Record<string, unknown>;
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource_type, id, params } = args;
|
||||
|
||||
const requestName = UPDATE_REQUEST_MAP[resource_type];
|
||||
if (!requestName) {
|
||||
throw new Error(
|
||||
`No UPDATE endpoint available for resource type: ${resource_type}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await client.rpc("write", requestName, { id, ...params });
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user