Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions packages/sawala-mcp/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import { formulirGetFormTool } from './formulir-get-form'
import { formulirGetSubmissionTool } from './formulir-get-submission'
import { formulirListFormsTool } from './formulir-list-forms'
import { formulirListSubmissionsTool } from './formulir-list-submissions'
import { kontenaCreateEntryTool } from './kontena-create-entry'
import { kontenaCreateSchemaTool } from './kontena-create-schema'
import { kontenaDeleteEntryTool } from './kontena-delete-entry'
import { kontenaDeleteSchemaTool } from './kontena-delete-schema'
import { kontenaGetEntryTool } from './kontena-get-entry'
import { kontenaGetSchemaTool } from './kontena-get-schema'
import { kontenaListEntriesTool } from './kontena-list-entries'
import { kontenaListSchemasTool } from './kontena-list-schemas'
import { kontenaPublishEntryTool } from './kontena-publish-entry'
import { kontenaUnpublishEntryTool } from './kontena-unpublish-entry'
import { kontenaUpdateEntryTool } from './kontena-update-entry'
import { kontenaUpdateSchemaTool } from './kontena-update-schema'
import { whoamiTool } from './whoami'

/**
Expand All @@ -19,8 +27,16 @@ export const ALL_TOOLS: ReadonlyArray<ToolDefinition<unknown>> = [
whoamiTool,
kontenaListSchemasTool,
kontenaGetSchemaTool,
kontenaCreateSchemaTool,
kontenaUpdateSchemaTool,
kontenaDeleteSchemaTool,
kontenaListEntriesTool,
kontenaGetEntryTool,
kontenaCreateEntryTool,
kontenaUpdateEntryTool,
kontenaDeleteEntryTool,
kontenaPublishEntryTool,
kontenaUnpublishEntryTool,
formulirListFormsTool,
formulirGetFormTool,
formulirListSubmissionsTool,
Expand Down
75 changes: 75 additions & 0 deletions packages/sawala-mcp/src/tools/kontena-create-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { z } from 'zod'
import { apiFetch } from '../lib/api-client'
import type { CliContext } from '../lib/auth'
import { zodParser, type ToolDefinition, type ToolInputSchema } from './types'

interface SchemaTypeResponse {
type: string
[k: string]: unknown
}

const inputZod = z
.object({
schemaSlug: z.string().min(1),
entry: z.record(z.string(), z.unknown()),
publish: z.boolean().optional(),
})
.strict()

type Input = z.infer<typeof inputZod>

const inputSchema: ToolInputSchema = {
type: 'object',
properties: {
schemaSlug: {
type: 'string',
description: 'Slug of the schema the entry belongs to.',
},
entry: {
type: 'object',
description:
'Entry body. Required: `locale`, `data`. Optional: `slug` (collection types only, auto-derived if omitted), ' +
'`status` (`draft` or `published`; defaults to `draft`), `publishedAt` (ISO 8601). ' +
'For single-type schemas this is an upsert per locale.',
},
publish: {
type: 'boolean',
description:
"Convenience flag: when true, sets `status='published'` in the same write " +
'(overrides any `status` in `entry`).',
},
},
required: ['schemaSlug', 'entry'],
additionalProperties: false,
}

export const kontenaCreateEntryTool: ToolDefinition<Input> = {
name: 'sawala_kontena_create_entry',
description:
'Create a Kontena content entry in the active project. Transparently fetches the schema first to route ' +
'single vs collection — callers think in terms of schemas, not in wire-protocol variants. Single-type ' +
'schemas upsert per locale; collection schemas enforce `(slug, locale)` uniqueness and 409 on duplicates.',
inputSchema,
annotations: { title: 'Create Kontena entry', readOnlyHint: false },
parseInput: zodParser(inputZod),
async handle(input: Input, ctx: CliContext) {
if (!ctx.activeProjectId) {
throw new Error(
'No active project id. Run `sawala project use <slug>` in a terminal to refresh, then retry.',
)
}
const projectId = ctx.activeProjectId
const payload: Record<string, unknown> = { ...input.entry }
if (input.publish) payload.status = 'published'
const schemaInfo = await apiFetch<SchemaTypeResponse>(
ctx,
`/cli/kontena/projects/${encodeURIComponent(projectId)}/schemas/${encodeURIComponent(input.schemaSlug)}`,
)
const subpath = schemaInfo.type === 'single' ? 'single' : 'collection'
return await apiFetch<unknown>(
ctx,
`/cli/kontena/projects/${encodeURIComponent(projectId)}/content/${subpath}/${encodeURIComponent(input.schemaSlug)}`,
{ method: 'POST', body: payload },
)
},
}
49 changes: 49 additions & 0 deletions packages/sawala-mcp/src/tools/kontena-create-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { z } from 'zod'
import { apiFetch } from '../lib/api-client'
import type { CliContext } from '../lib/auth'
import { zodParser, type ToolDefinition, type ToolInputSchema } from './types'

const inputZod = z
.object({
schema: z.record(z.string(), z.unknown()),
})
.strict()

type Input = z.infer<typeof inputZod>

const inputSchema: ToolInputSchema = {
type: 'object',
properties: {
schema: {
type: 'object',
description:
'Schema body to create. Required fields: `name`, `type` (`single` or `collection`), `fields`. ' +
'Optional: `slug` (auto-derived from name if omitted), `locales`, `staticExport`, `indexFields`.',
},
},
required: ['schema'],
additionalProperties: false,
}

export const kontenaCreateSchemaTool: ToolDefinition<Input> = {
name: 'sawala_kontena_create_schema',
description:
'Create a new Kontena content schema in the active project. The `schema` input is sent as the POST body; ' +
'see the Kontena docs for the field-definition shape. Returns the created schema row. The backend rejects ' +
'duplicate slugs with HTTP 409. Requires admin role on the active org.',
inputSchema,
annotations: { title: 'Create Kontena schema', readOnlyHint: false },
parseInput: zodParser(inputZod),
async handle(input: Input, ctx: CliContext) {
if (!ctx.activeProjectId) {
throw new Error(
'No active project id. Run `sawala project use <slug>` in a terminal to refresh, then retry.',
)
}
return await apiFetch<unknown>(
ctx,
`/cli/kontena/projects/${encodeURIComponent(ctx.activeProjectId)}/schemas`,
{ method: 'POST', body: input.schema },
)
},
}
82 changes: 82 additions & 0 deletions packages/sawala-mcp/src/tools/kontena-delete-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { z } from 'zod'
import { apiFetch } from '../lib/api-client'
import type { CliContext } from '../lib/auth'
import { zodParser, type ToolDefinition, type ToolInputSchema } from './types'

interface SchemaTypeResponse {
type: string
[k: string]: unknown
}

const inputZod = z
.object({
schemaSlug: z.string().min(1),
slugOrId: z.string().min(1),
locale: z.string().optional(),
confirm: z.literal(true),
})
.strict()

type Input = z.infer<typeof inputZod>

const inputSchema: ToolInputSchema = {
type: 'object',
properties: {
schemaSlug: {
type: 'string',
description: 'Slug of the schema the entry belongs to.',
},
slugOrId: {
type: 'string',
description:
'Entry ULID or slug. Ignored for single-type schemas (they target by locale instead).',
},
locale: {
type: 'string',
description: 'Locale to target (required for single-type schemas).',
},
confirm: {
type: 'boolean',
enum: [true],
description:
'Must be `true` to acknowledge the destructive nature. Guards against accidental empty-input invocations.',
},
},
required: ['schemaSlug', 'slugOrId', 'confirm'],
additionalProperties: false,
}

export const kontenaDeleteEntryTool: ToolDefinition<Input> = {
name: 'sawala_kontena_delete_entry',
description:
'Delete a Kontena content entry. **Destructive** — also fails (HTTP 409) if the entry is currently ' +
'published; unpublish first or run `sawala_kontena_unpublish_entry` before retrying. Transparently ' +
'fetches the schema to route single vs collection. MCP hosts SHOULD surface this call for human approval.',
inputSchema,
annotations: {
title: 'Delete Kontena entry',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
irreversibleHint: true,
},
parseInput: zodParser(inputZod),
async handle(input: Input, ctx: CliContext) {
if (!ctx.activeProjectId) {
throw new Error(
'No active project id. Run `sawala project use <slug>` in a terminal to refresh, then retry.',
)
}
const projectId = ctx.activeProjectId
const schemaInfo = await apiFetch<SchemaTypeResponse>(
ctx,
`/cli/kontena/projects/${encodeURIComponent(projectId)}/schemas/${encodeURIComponent(input.schemaSlug)}`,
)
const url =
schemaInfo.type === 'single'
? `/cli/kontena/projects/${encodeURIComponent(projectId)}/content/single/${encodeURIComponent(input.schemaSlug)}` +
(input.locale ? `?locale=${encodeURIComponent(input.locale)}` : '')
: `/cli/kontena/projects/${encodeURIComponent(projectId)}/content/collection/${encodeURIComponent(input.schemaSlug)}/${encodeURIComponent(input.slugOrId)}`
return await apiFetch<unknown>(ctx, url, { method: 'DELETE' })
},
}
61 changes: 61 additions & 0 deletions packages/sawala-mcp/src/tools/kontena-delete-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { z } from 'zod'
import { apiFetch } from '../lib/api-client'
import type { CliContext } from '../lib/auth'
import { zodParser, type ToolDefinition, type ToolInputSchema } from './types'

const inputZod = z
.object({
slugOrId: z.string().min(1),
confirm: z.literal(true),
})
.strict()

type Input = z.infer<typeof inputZod>

const inputSchema: ToolInputSchema = {
type: 'object',
properties: {
slugOrId: {
type: 'string',
description: 'Schema ULID or human-readable slug to delete.',
},
confirm: {
type: 'boolean',
enum: [true],
description:
'Must be `true` to acknowledge the destructive nature of this call. ' +
'Guards against accidental empty-input invocations.',
},
},
required: ['slugOrId', 'confirm'],
additionalProperties: false,
}

export const kontenaDeleteSchemaTool: ToolDefinition<Input> = {
name: 'sawala_kontena_delete_schema',
description:
'Delete a Kontena content schema in the active project. **Destructive** — also fails (HTTP 409) if any ' +
'entries still reference the schema; delete the entries first. Pass `confirm: true` to acknowledge. ' +
'MCP hosts SHOULD surface this call for human approval before executing.',
inputSchema,
annotations: {
title: 'Delete Kontena schema',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
irreversibleHint: true,
},
parseInput: zodParser(inputZod),
async handle(input: Input, ctx: CliContext) {
if (!ctx.activeProjectId) {
throw new Error(
'No active project id. Run `sawala project use <slug>` in a terminal to refresh, then retry.',
)
}
return await apiFetch<unknown>(
ctx,
`/cli/kontena/projects/${encodeURIComponent(ctx.activeProjectId)}/schemas/${encodeURIComponent(input.slugOrId)}`,
{ method: 'DELETE' },
)
},
}
57 changes: 57 additions & 0 deletions packages/sawala-mcp/src/tools/kontena-publish-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { z } from 'zod'
import { apiFetch } from '../lib/api-client'
import type { CliContext } from '../lib/auth'
import { zodParser, type ToolDefinition, type ToolInputSchema } from './types'

const inputZod = z
.object({
schemaSlug: z.string().min(1),
slugOrId: z.string().min(1),
})
.strict()

type Input = z.infer<typeof inputZod>

const inputSchema: ToolInputSchema = {
type: 'object',
properties: {
schemaSlug: {
type: 'string',
description: 'Slug of the collection schema the entry belongs to.',
},
slugOrId: {
type: 'string',
description: 'Entry ULID or slug to publish.',
},
},
required: ['schemaSlug', 'slugOrId'],
additionalProperties: false,
}

export const kontenaPublishEntryTool: ToolDefinition<Input> = {
name: 'sawala_kontena_publish_entry',
description:
"Publish a Kontena collection-entry draft (sets `status='published'`). Idempotent: re-publishing an " +
'already-published entry is a no-op at the wire level. v1 supports collection schemas only; for ' +
'single-type schemas use `sawala_kontena_update_entry` with `publish: true` and the locale in `patch`.',
inputSchema,
annotations: {
title: 'Publish Kontena entry',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
parseInput: zodParser(inputZod),
async handle(input: Input, ctx: CliContext) {
if (!ctx.activeProjectId) {
throw new Error(
'No active project id. Run `sawala project use <slug>` in a terminal to refresh, then retry.',
)
}
return await apiFetch<unknown>(
ctx,
`/cli/kontena/projects/${encodeURIComponent(ctx.activeProjectId)}/content/collection/${encodeURIComponent(input.schemaSlug)}/${encodeURIComponent(input.slugOrId)}`,
{ method: 'PUT', body: { status: 'published' } },
)
},
}
Loading
Loading