diff --git a/bun.lock b/bun.lock index c57766a4..b0253431 100644 --- a/bun.lock +++ b/bun.lock @@ -146,6 +146,19 @@ "wrangler": "^4.28.0", }, }, + "deco-admin-airtable-sync": { + "name": "@decocms/deco-admin-airtable-sync", + "version": "1.0.0", + "dependencies": { + "@decocms/runtime": "1.2.8", + "zod": "^4.0.0", + }, + "devDependencies": { + "@decocms/mcps-shared": "1.0.0", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, "deco-llm": { "name": "deco-llm", "version": "1.0.0", @@ -1313,6 +1326,8 @@ "@decocms/bindings": ["@decocms/bindings@1.0.7", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.1", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-NPYv4+VpI6XQbfMewy307Q1jp9QZc8a6lsC2g9Z/DCewKqFOCqAKsRrhBSGaujKEzHqxNLSqXhFx8/Vn3ODVJA=="], + "@decocms/deco-admin-airtable-sync": ["@decocms/deco-admin-airtable-sync@workspace:deco-admin-airtable-sync"], + "@decocms/mcps-shared": ["@decocms/mcps-shared@workspace:shared"], "@decocms/openrouter": ["@decocms/openrouter@workspace:openrouter"], @@ -3651,6 +3666,10 @@ "@decocms/bindings/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@decocms/deco-admin-airtable-sync/@decocms/runtime": ["@decocms/runtime@1.2.8", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.3", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-AG0nqc7ofe7oGeqQpqoqsmSo2t9bZ+xEVnhHDXKQqwmGgXHwRhfVTzROqUq0rasTGnrnTKKSRVLgsHWUgfCaxA=="], + + "@decocms/deco-admin-airtable-sync/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@decocms/mcps-shared/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "@decocms/mcps-shared/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -4443,6 +4462,10 @@ "@decocms/airtable/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@decocms/deco-admin-airtable-sync/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + + "@decocms/deco-admin-airtable-sync/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@decocms/mcps-shared/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], "@decocms/openrouter/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], diff --git a/deco-admin-airtable-sync/app.json b/deco-admin-airtable-sync/app.json new file mode 100644 index 00000000..3ed7f51b --- /dev/null +++ b/deco-admin-airtable-sync/app.json @@ -0,0 +1,24 @@ +{ + "scopeName": "deco", + "name": "deco-admin-airtable-sync", + "friendlyName": "Deco Admin — Airtable Sync", + "connection": { + "type": "HTTP", + "url": "https://sites-deco-admin-airtable-sync.decocache.com/mcp" + }, + "description": "Triggers the deco.cx admin Airtable sync action for a given table.", + "icon": "https://decoims.com/admin/81eca612-7d01-4a91-80f9-5081e277ba61/invoice.png", + "unlisted": true, + "auth": { + "type": "token", + "header": "Authorization", + "prefix": "Bearer" + }, + "metadata": { + "categories": ["Developer Tools"], + "official": false, + "tags": ["airtable", "deco", "sync", "admin"], + "short_description": "Triggers the deco.cx admin Airtable sync action.", + "mesh_description": "This MCP provides a single tool to trigger the deco.cx admin Airtable sync action for a specific table. It allows AI agents to kick off data synchronization jobs between Airtable and the deco.cx platform, with support for dry-run mode and workflow orchestration." + } +} diff --git a/deco-admin-airtable-sync/package.json b/deco-admin-airtable-sync/package.json new file mode 100644 index 00000000..26c0be28 --- /dev/null +++ b/deco-admin-airtable-sync/package.json @@ -0,0 +1,23 @@ +{ + "name": "@decocms/deco-admin-airtable-sync", + "version": "1.0.0", + "description": "MCP to trigger deco.cx admin Airtable sync actions", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot server/main.ts", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "publish": "cat app.json | deco registry publish -w /shared/deco -y", + "check": "tsc --noEmit" + }, + "dependencies": { + "@decocms/runtime": "1.2.8", + "zod": "^4.0.0" + }, + "devDependencies": { + "@decocms/mcps-shared": "1.0.0", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2" + } +} diff --git a/deco-admin-airtable-sync/server/lib/env.ts b/deco-admin-airtable-sync/server/lib/env.ts new file mode 100644 index 00000000..62c87db9 --- /dev/null +++ b/deco-admin-airtable-sync/server/lib/env.ts @@ -0,0 +1,6 @@ +import type { Env } from "../../shared/deco.gen.ts"; + +export function getApiKey(env: Env): string { + const auth = env.MESH_REQUEST_CONTEXT?.authorization ?? ""; + return auth.startsWith("Bearer ") ? auth.slice(7) : auth; +} diff --git a/deco-admin-airtable-sync/server/main.ts b/deco-admin-airtable-sync/server/main.ts new file mode 100644 index 00000000..ed95ba09 --- /dev/null +++ b/deco-admin-airtable-sync/server/main.ts @@ -0,0 +1,14 @@ +import { withRuntime } from "@decocms/runtime"; +import { serve } from "@decocms/mcps-shared/serve"; +import { tools } from "./tools/index.ts"; +import type { Env } from "../shared/deco.gen.ts"; + +export type { Env }; + +const runtime = withRuntime({ + tools: (env: Env) => tools.map((createTool) => createTool(env)), +}); + +if (runtime.fetch) { + serve(runtime.fetch); +} diff --git a/deco-admin-airtable-sync/server/tools/index.ts b/deco-admin-airtable-sync/server/tools/index.ts new file mode 100644 index 00000000..cd5ea902 --- /dev/null +++ b/deco-admin-airtable-sync/server/tools/index.ts @@ -0,0 +1,3 @@ +import { createSyncAirtableTable } from "./sync-airtable-table.ts"; + +export const tools = [createSyncAirtableTable]; diff --git a/deco-admin-airtable-sync/server/tools/sync-airtable-table.ts b/deco-admin-airtable-sync/server/tools/sync-airtable-table.ts new file mode 100644 index 00000000..cc0d6e9e --- /dev/null +++ b/deco-admin-airtable-sync/server/tools/sync-airtable-table.ts @@ -0,0 +1,62 @@ +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../../shared/deco.gen.ts"; +import { getApiKey } from "../lib/env.ts"; + +const SYNC_URL = + "https://admin.deco.cx/live/invoke/deco-sites/admin/actions/airtable/sync.ts"; + +export const createSyncAirtableTable = (env: Env) => + createPrivateTool({ + id: "syncAirtableTable", + description: + "Triggers the deco.cx admin Airtable sync action for a given table. " + + "Use mode 'dry-run' to preview changes without applying them.", + inputSchema: z.object({ + table: z + .string() + .describe('Name of the Airtable table to sync, e.g. "invoices"'), + shouldStartWorkflow: z + .boolean() + .default(false) + .describe("Whether to start a workflow after the sync completes"), + mode: z + .enum(["apply", "dry-run"]) + .default("apply") + .describe( + '"apply" executes the sync for real; "dry-run" simulates without making changes', + ), + }), + outputSchema: z.object({ + ok: z.boolean(), + status: z.number(), + body: z.unknown(), + }), + execute: async ({ context }) => { + const apiKey = getApiKey(env); + const { table, shouldStartWorkflow, mode } = context; + + const res = await fetch(SYNC_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + body: JSON.stringify({ table, shouldStartWorkflow, mode }), + }); + + const text = await res.text(); + let body: unknown = text; + try { + body = JSON.parse(text); + } catch { + // keep as plain text + } + + if (!res.ok) { + throw new Error(`Sync failed (${res.status}): ${text}`); + } + + return { ok: true, status: res.status, body }; + }, + }); diff --git a/deco-admin-airtable-sync/shared/deco.gen.ts b/deco-admin-airtable-sync/shared/deco.gen.ts new file mode 100644 index 00000000..df147902 --- /dev/null +++ b/deco-admin-airtable-sync/shared/deco.gen.ts @@ -0,0 +1,7 @@ +export interface MeshRequestContext { + authorization?: string; +} + +export interface Env { + MESH_REQUEST_CONTEXT: MeshRequestContext; +} diff --git a/deco-admin-airtable-sync/tsconfig.json b/deco-admin-airtable-sync/tsconfig.json new file mode 100644 index 00000000..93cae12f --- /dev/null +++ b/deco-admin-airtable-sync/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "ES2024", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "allowJs": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "server/*": ["./server/*"] + } + }, + "include": [ + "server" + ] +} diff --git a/deploy.json b/deploy.json index 93e224cd..10e43fe8 100644 --- a/deploy.json +++ b/deploy.json @@ -1,4 +1,13 @@ { + "deco-admin-airtable-sync": { + "site": "deco-admin-airtable-sync", + "entrypoint": "./dist/server/main.js", + "platformName": "kubernetes-bun", + "watch": [ + "deco-admin-airtable-sync/**", + "shared/**" + ] + }, "airtable": { "site": "airtable", "entrypoint": "./dist/server/main.js", diff --git a/package.json b/package.json index cbf15617..7907f190 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "crazy-egg", "data-for-seo", "datajud", + "deco-admin-airtable-sync", "deco-llm", "deco-news-weekly-digest", "discord",