@formmy.app/mcp-server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerAgentTools } from "./tools/agents.js";
5
+ import { registerChatTools } from "./tools/chat.js";
6
+ import { registerDocumentTools } from "./tools/documents.js";
7
+ import { registerConversationTools } from "./tools/conversations.js";
8
+ const server = new McpServer({
9
+ name: "formmy",
10
+ version: "0.1.0",
11
+ });
12
+ registerAgentTools(server);
13
+ registerChatTools(server);
14
+ registerDocumentTools(server);
15
+ registerConversationTools(server);
16
+ const transport = new StdioServerTransport();
17
+ await server.connect(transport);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * HTTP client for Formmy SDK API
3
+ * Wraps all calls to /api/v2/sdk with proper auth
4
+ */
5
+ export declare function listAgents(): Promise<unknown>;
6
+ export declare function getAgent(agentId: string): Promise<unknown>;
7
+ export declare function createAgent(data: {
8
+ name: string;
9
+ instructions?: string;
10
+ welcomeMessage?: string;
11
+ model?: string;
12
+ }): Promise<unknown>;
13
+ export declare function updateAgent(agentId: string, data: {
14
+ name?: string;
15
+ instructions?: string;
16
+ customInstructions?: string;
17
+ welcomeMessage?: string;
18
+ model?: string;
19
+ }): Promise<unknown>;
20
+ export declare function chatWithAgent(agentId: string, message: string, sessionId?: string): Promise<string>;
21
+ export declare function addDocument(agentId: string, title: string, content: string): Promise<unknown>;
22
+ export declare function listDocuments(agentId: string): Promise<unknown>;
23
+ export declare function getDocument(documentId: string): Promise<unknown>;
24
+ export declare function searchDocuments(agentId: string, query: string): Promise<unknown>;
25
+ export declare function updateDocument(documentId: string, data: {
26
+ title?: string;
27
+ content?: string;
28
+ }): Promise<unknown>;
29
+ export declare function deleteDocument(documentId: string): Promise<unknown>;
30
+ export declare function listConversations(agentId: string): Promise<unknown>;
31
+ export declare function getConversation(conversationId: string): Promise<unknown>;
32
+ export declare function setConversationStatus(conversationId: string, label: string, color: string): Promise<unknown>;
33
+ export declare function addConversationTag(conversationId: string, label: string, color: string, comment?: string): Promise<unknown>;
34
+ export declare function removeConversationTag(conversationId: string, tagLabel: string): Promise<unknown>;
35
+ export declare function shareAgent(agentId: string, email: string, role?: string): Promise<unknown>;
36
+ export declare function connectIntegration(agentId: string, integration: string, config: Record<string, string>): Promise<unknown>;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * HTTP client for Formmy SDK API
3
+ * Wraps all calls to /api/v2/sdk with proper auth
4
+ */
5
+ const API_URL = process.env.FORMMY_API_URL || "https://formmy.app";
6
+ const SECRET_KEY = process.env.FORMMY_SECRET_KEY || "";
7
+ if (!SECRET_KEY) {
8
+ console.error("[formmy-mcp] FORMMY_SECRET_KEY is required");
9
+ }
10
+ async function sdkFetch(intent, options = {}) {
11
+ const { method = "GET", params = {}, body } = options;
12
+ const url = new URL(`${API_URL}/api/v2/sdk`);
13
+ url.searchParams.set("intent", intent);
14
+ for (const [k, v] of Object.entries(params)) {
15
+ url.searchParams.set(k, v);
16
+ }
17
+ const headers = {
18
+ "X-Secret-Key": SECRET_KEY,
19
+ };
20
+ if (body) {
21
+ headers["Content-Type"] = "application/json";
22
+ }
23
+ const res = await fetch(url.toString(), {
24
+ method,
25
+ headers,
26
+ body: body ? JSON.stringify(body) : undefined,
27
+ });
28
+ const data = await res.json();
29
+ if (!res.ok) {
30
+ const msg = data.error || `SDK error ${res.status}`;
31
+ throw new Error(msg);
32
+ }
33
+ return data;
34
+ }
35
+ // --- Agents ---
36
+ export async function listAgents() {
37
+ return sdkFetch("agents.list");
38
+ }
39
+ export async function getAgent(agentId) {
40
+ return sdkFetch("agents.get", { params: { agentId } });
41
+ }
42
+ export async function createAgent(data) {
43
+ return sdkFetch("agents.create", { method: "POST", body: data });
44
+ }
45
+ export async function updateAgent(agentId, data) {
46
+ return sdkFetch("agents.update", {
47
+ method: "POST",
48
+ params: { agentId },
49
+ body: data,
50
+ });
51
+ }
52
+ // --- Chat ---
53
+ export async function chatWithAgent(agentId, message, sessionId) {
54
+ const sid = sessionId || `mcp_${Date.now()}`;
55
+ const res = await fetch(`${API_URL}/api/v2/sdk?intent=chat&agentId=${agentId}`, {
56
+ method: "POST",
57
+ headers: {
58
+ "X-Secret-Key": SECRET_KEY,
59
+ "Content-Type": "application/json",
60
+ },
61
+ body: JSON.stringify({
62
+ id: sid,
63
+ message: {
64
+ parts: [{ type: "text", text: message }],
65
+ },
66
+ }),
67
+ });
68
+ if (!res.ok) {
69
+ const err = await res.json().catch(() => ({}));
70
+ throw new Error(err.error || `Chat error ${res.status}`);
71
+ }
72
+ // Streaming response — collect all text chunks
73
+ const text = await res.text();
74
+ // The SDK returns SSE-style data stream, extract text parts
75
+ const lines = text.split("\n");
76
+ const parts = [];
77
+ for (const line of lines) {
78
+ if (line.startsWith("0:")) {
79
+ // Vercel AI SDK text token format: 0:"text content"
80
+ try {
81
+ parts.push(JSON.parse(line.slice(2)));
82
+ }
83
+ catch {
84
+ // skip malformed
85
+ }
86
+ }
87
+ }
88
+ return parts.join("") || text;
89
+ }
90
+ // --- Documents ---
91
+ export async function addDocument(agentId, title, content) {
92
+ return sdkFetch("documents.create", {
93
+ method: "POST",
94
+ params: { agentId },
95
+ body: { title, content },
96
+ });
97
+ }
98
+ export async function listDocuments(agentId) {
99
+ return sdkFetch("documents.list", { params: { agentId } });
100
+ }
101
+ export async function getDocument(documentId) {
102
+ return sdkFetch("documents.get", { params: { documentId } });
103
+ }
104
+ export async function searchDocuments(agentId, query) {
105
+ return sdkFetch("documents.search", { params: { agentId, query } });
106
+ }
107
+ export async function updateDocument(documentId, data) {
108
+ return sdkFetch("documents.update", {
109
+ method: "PUT",
110
+ params: { documentId },
111
+ body: data,
112
+ });
113
+ }
114
+ export async function deleteDocument(documentId) {
115
+ return sdkFetch("documents.delete", {
116
+ method: "DELETE",
117
+ params: { documentId },
118
+ });
119
+ }
120
+ // --- Conversations ---
121
+ export async function listConversations(agentId) {
122
+ return sdkFetch("conversations.list", { params: { agentId } });
123
+ }
124
+ export async function getConversation(conversationId) {
125
+ return sdkFetch("conversations.get", { params: { conversationId } });
126
+ }
127
+ // --- Agent bridge mutations (NANOCLAW_WEBHOOK_SECRET auth) ---
128
+ // Pega a /api/v1/agents/conversations, no a /api/v2/sdk. Usa Bearer con el
129
+ // mismo secret que ya autentica el puente Formmy ↔ NanoClaw bidireccional,
130
+ // para no introducir env nuevas en el droplet de sofi-0.
131
+ const BRIDGE_SECRET = process.env.NANOCLAW_WEBHOOK_SECRET || "";
132
+ async function bridgeFetch(intent, body) {
133
+ if (!BRIDGE_SECRET) {
134
+ throw new Error("NANOCLAW_WEBHOOK_SECRET no está configurado en el entorno");
135
+ }
136
+ const url = new URL(`${API_URL}/api/v1/agents/conversations`);
137
+ url.searchParams.set("intent", intent);
138
+ const res = await fetch(url.toString(), {
139
+ method: "POST",
140
+ headers: {
141
+ Authorization: `Bearer ${BRIDGE_SECRET}`,
142
+ "Content-Type": "application/json",
143
+ },
144
+ body: JSON.stringify(body),
145
+ });
146
+ const data = await res.json().catch(() => ({}));
147
+ if (!res.ok) {
148
+ const msg = data.error || `Bridge error ${res.status}`;
149
+ throw new Error(msg);
150
+ }
151
+ return data;
152
+ }
153
+ export async function setConversationStatus(conversationId, label, color) {
154
+ return bridgeFetch("set_estado", { conversationId, label, color });
155
+ }
156
+ export async function addConversationTag(conversationId, label, color, comment) {
157
+ return bridgeFetch("add_tag", { conversationId, label, color, comment });
158
+ }
159
+ export async function removeConversationTag(conversationId, tagLabel) {
160
+ return bridgeFetch("remove_tag", { conversationId, tagLabel });
161
+ }
162
+ // --- Sharing ---
163
+ export async function shareAgent(agentId, email, role) {
164
+ return sdkFetch("agents.share", {
165
+ method: "POST",
166
+ params: { agentId },
167
+ body: { email, role },
168
+ });
169
+ }
170
+ // --- Integrations ---
171
+ export async function connectIntegration(agentId, integration, config) {
172
+ return sdkFetch("integrations.connect", {
173
+ method: "POST",
174
+ params: { agentId },
175
+ body: { integration, config },
176
+ });
177
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAgentTools(server: McpServer): void;
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ import * as sdk from "../sdk-client.js";
3
+ export function registerAgentTools(server) {
4
+ server.tool("list_agents", "List all Formmy agents for the authenticated user", {}, async () => {
5
+ const result = await sdk.listAgents();
6
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7
+ });
8
+ server.tool("get_agent", "Get details of a specific Formmy agent", { agentId: z.string().describe("Agent ID or slug") }, async ({ agentId }) => {
9
+ const result = await sdk.getAgent(agentId);
10
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11
+ });
12
+ server.tool("create_agent", "Create a new Formmy agent/chatbot", {
13
+ name: z.string().describe("Agent name"),
14
+ instructions: z.string().optional().describe("System prompt / instructions"),
15
+ welcomeMessage: z.string().optional().describe("Welcome message shown to users"),
16
+ model: z.string().optional().describe("AI model (e.g. gpt-4o-mini, gpt-4.1-mini, claude-3-5-haiku-latest)"),
17
+ }, async (params) => {
18
+ const result = await sdk.createAgent(params);
19
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
20
+ });
21
+ server.tool("update_agent", "Update an existing Formmy agent", {
22
+ agentId: z.string().describe("Agent ID or slug"),
23
+ name: z.string().optional().describe("New name"),
24
+ instructions: z.string().optional().describe("New system prompt"),
25
+ customInstructions: z.string().optional().describe("Additional custom instructions"),
26
+ welcomeMessage: z.string().optional().describe("New welcome message"),
27
+ model: z.string().optional().describe("New AI model"),
28
+ }, async ({ agentId, ...data }) => {
29
+ const result = await sdk.updateAgent(agentId, data);
30
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
31
+ });
32
+ server.tool("share_agent", "Share a Formmy agent with another user by email. Creates a permission so they can see and manage the agent.", {
33
+ agentId: z.string().describe("Agent ID or slug"),
34
+ email: z.string().describe("Email of the user to share with"),
35
+ role: z.enum(["VIEWER", "EDITOR", "ADMIN"]).optional().describe("Role to grant (default: VIEWER)"),
36
+ }, async ({ agentId, email, role }) => {
37
+ const result = await sdk.shareAgent(agentId, email, role);
38
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
39
+ });
40
+ server.tool("connect_integration", "Connect an external service (e.g. EasyBits DB) to a Formmy agent. The agent gets tools to interact with the service at chat time.", {
41
+ agentId: z.string().describe("Agent ID or slug"),
42
+ integration: z.string().describe("Integration name (e.g. 'easybits')"),
43
+ config: z.record(z.string()).describe("Integration config (e.g. { apiKey, dbId })"),
44
+ }, async ({ agentId, integration, config }) => {
45
+ const result = await sdk.connectIntegration(agentId, integration, config);
46
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
47
+ });
48
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerChatTools(server: McpServer): void;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import * as sdk from "../sdk-client.js";
3
+ export function registerChatTools(server) {
4
+ server.tool("chat_with_agent", "Send a message to a Formmy agent and get the full response (non-streaming)", {
5
+ agentId: z.string().describe("Agent ID or slug"),
6
+ message: z.string().describe("Message to send"),
7
+ sessionId: z.string().optional().describe("Session ID for conversation continuity"),
8
+ }, async ({ agentId, message, sessionId }) => {
9
+ const response = await sdk.chatWithAgent(agentId, message, sessionId);
10
+ return { content: [{ type: "text", text: response }] };
11
+ });
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerConversationTools(server: McpServer): void;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import * as sdk from "../sdk-client.js";
3
+ export function registerConversationTools(server) {
4
+ server.tool("list_conversations", "List conversations for a Formmy agent", {
5
+ agentId: z.string().describe("Agent ID or slug"),
6
+ }, async ({ agentId }) => {
7
+ const result = await sdk.listConversations(agentId);
8
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
9
+ });
10
+ server.tool("get_conversation", "Get a conversation with its messages", {
11
+ conversationId: z.string().describe("Conversation ID"),
12
+ }, async ({ conversationId }) => {
13
+ const result = await sdk.getConversation(conversationId);
14
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
15
+ });
16
+ server.tool("set_conversation_status", "Set the CRM status (estado) of a conversation. Use when the user signals a clear state change: payment confirmed → 'Pago confirmado' (#10B981), needs human → 'Solo operador' (#3B82F6), in progress → 'Atendiendo' (#F59E0B), resolved → 'Atendido' (#10B981). label is free-form text; color must be a hex string. The operator sees the new chip in the conversations list immediately.", {
17
+ conversationId: z.string().describe("Conversation ID"),
18
+ label: z.string().describe("Status label, e.g. 'Pago confirmado', 'Solo operador'"),
19
+ color: z.string().describe("Hex color, e.g. '#10B981' (green), '#3B82F6' (blue), '#F59E0B' (amber), '#EF4444' (red)"),
20
+ }, async ({ conversationId, label, color }) => {
21
+ const result = await sdk.setConversationStatus(conversationId, label, color);
22
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
23
+ });
24
+ server.tool("add_conversation_tag", "Add a CRM tag to a conversation. Use to mark categorical attributes the operator should see at a glance: 'VIP', 'lead', 'urgente', 'cotización', etc. label is free-form; color is hex. Optional comment is internal note (not shown to the client). Returns the updated tag list.", {
25
+ conversationId: z.string().describe("Conversation ID"),
26
+ label: z.string().describe("Tag label, e.g. 'VIP', 'lead', 'urgente'"),
27
+ color: z.string().describe("Hex color, e.g. '#A855F7' (purple/VIP), '#10B981' (green/lead), '#EF4444' (red/urgente)"),
28
+ comment: z.string().optional().describe("Optional internal note about why this tag was applied"),
29
+ }, async ({ conversationId, label, color, comment }) => {
30
+ const result = await sdk.addConversationTag(conversationId, label, color, comment);
31
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
32
+ });
33
+ server.tool("remove_conversation_tag", "Remove a CRM tag from a conversation by its label. Case-insensitive match.", {
34
+ conversationId: z.string().describe("Conversation ID"),
35
+ tagLabel: z.string().describe("Label of the tag to remove"),
36
+ }, async ({ conversationId, tagLabel }) => {
37
+ const result = await sdk.removeConversationTag(conversationId, tagLabel);
38
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
39
+ });
40
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerDocumentTools(server: McpServer): void;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import * as sdk from "../sdk-client.js";
3
+ export function registerDocumentTools(server) {
4
+ server.tool("add_document", "Add a RAG document/knowledge base entry to a Formmy agent", {
5
+ agentId: z.string().describe("Agent ID or slug"),
6
+ title: z.string().describe("Document title"),
7
+ content: z.string().describe("Document content (text, markdown, etc.)"),
8
+ }, async ({ agentId, title, content }) => {
9
+ const result = await sdk.addDocument(agentId, title, content);
10
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11
+ });
12
+ server.tool("list_documents", "List all RAG documents for a Formmy agent", {
13
+ agentId: z.string().describe("Agent ID or slug"),
14
+ }, async ({ agentId }) => {
15
+ const result = await sdk.listDocuments(agentId);
16
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
17
+ });
18
+ server.tool("get_document", "Get a specific RAG document with its full content", {
19
+ documentId: z.string().describe("Document ID"),
20
+ }, async ({ documentId }) => {
21
+ const result = await sdk.getDocument(documentId);
22
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
23
+ });
24
+ server.tool("search_documents", "Semantic search across an agent's RAG documents", {
25
+ agentId: z.string().describe("Agent ID or slug"),
26
+ query: z.string().describe("Search query"),
27
+ }, async ({ agentId, query }) => {
28
+ const result = await sdk.searchDocuments(agentId, query);
29
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
30
+ });
31
+ server.tool("update_document", "Update an existing RAG document's title or content", {
32
+ documentId: z.string().describe("Document ID"),
33
+ title: z.string().optional().describe("New title"),
34
+ content: z.string().optional().describe("New content"),
35
+ }, async ({ documentId, ...data }) => {
36
+ const result = await sdk.updateDocument(documentId, data);
37
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38
+ });
39
+ server.tool("delete_document", "Delete a RAG document from an agent", {
40
+ documentId: z.string().describe("Document ID"),
41
+ }, async ({ documentId }) => {
42
+ const result = await sdk.deleteDocument(documentId);
43
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
44
+ });
45
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@formmy.app/mcp-server",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for managing Formmy agents via Claude Code",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "formmy-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "clean": "rm -rf dist"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.12.1"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.8.3",
20
+ "@types/node": "^22.0.0"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerAgentTools } from "./tools/agents.js";
5
+ import { registerChatTools } from "./tools/chat.js";
6
+ import { registerDocumentTools } from "./tools/documents.js";
7
+ import { registerConversationTools } from "./tools/conversations.js";
8
+
9
+ const server = new McpServer({
10
+ name: "formmy",
11
+ version: "0.1.0",
12
+ });
13
+
14
+ registerAgentTools(server);
15
+ registerChatTools(server);
16
+ registerDocumentTools(server);
17
+ registerConversationTools(server);
18
+
19
+ const transport = new StdioServerTransport();
20
+ await server.connect(transport);
@@ -0,0 +1,269 @@
1
+ /**
2
+ * HTTP client for Formmy SDK API
3
+ * Wraps all calls to /api/v2/sdk with proper auth
4
+ */
5
+
6
+ const API_URL = process.env.FORMMY_API_URL || "https://formmy.app";
7
+ const SECRET_KEY = process.env.FORMMY_SECRET_KEY || "";
8
+
9
+ if (!SECRET_KEY) {
10
+ console.error("[formmy-mcp] FORMMY_SECRET_KEY is required");
11
+ }
12
+
13
+ async function sdkFetch(
14
+ intent: string,
15
+ options: {
16
+ method?: "GET" | "POST" | "PUT" | "DELETE";
17
+ params?: Record<string, string>;
18
+ body?: unknown;
19
+ } = {}
20
+ ): Promise<unknown> {
21
+ const { method = "GET", params = {}, body } = options;
22
+
23
+ const url = new URL(`${API_URL}/api/v2/sdk`);
24
+ url.searchParams.set("intent", intent);
25
+ for (const [k, v] of Object.entries(params)) {
26
+ url.searchParams.set(k, v);
27
+ }
28
+
29
+ const headers: Record<string, string> = {
30
+ "X-Secret-Key": SECRET_KEY,
31
+ };
32
+ if (body) {
33
+ headers["Content-Type"] = "application/json";
34
+ }
35
+
36
+ const res = await fetch(url.toString(), {
37
+ method,
38
+ headers,
39
+ body: body ? JSON.stringify(body) : undefined,
40
+ });
41
+
42
+ const data = await res.json();
43
+
44
+ if (!res.ok) {
45
+ const msg = (data as { error?: string }).error || `SDK error ${res.status}`;
46
+ throw new Error(msg);
47
+ }
48
+
49
+ return data;
50
+ }
51
+
52
+ // --- Agents ---
53
+
54
+ export async function listAgents() {
55
+ return sdkFetch("agents.list");
56
+ }
57
+
58
+ export async function getAgent(agentId: string) {
59
+ return sdkFetch("agents.get", { params: { agentId } });
60
+ }
61
+
62
+ export async function createAgent(data: {
63
+ name: string;
64
+ instructions?: string;
65
+ welcomeMessage?: string;
66
+ model?: string;
67
+ }) {
68
+ return sdkFetch("agents.create", { method: "POST", body: data });
69
+ }
70
+
71
+ export async function updateAgent(
72
+ agentId: string,
73
+ data: {
74
+ name?: string;
75
+ instructions?: string;
76
+ customInstructions?: string;
77
+ welcomeMessage?: string;
78
+ model?: string;
79
+ }
80
+ ) {
81
+ return sdkFetch("agents.update", {
82
+ method: "POST",
83
+ params: { agentId },
84
+ body: data,
85
+ });
86
+ }
87
+
88
+ // --- Chat ---
89
+
90
+ export async function chatWithAgent(
91
+ agentId: string,
92
+ message: string,
93
+ sessionId?: string
94
+ ) {
95
+ const sid = sessionId || `mcp_${Date.now()}`;
96
+ const res = await fetch(`${API_URL}/api/v2/sdk?intent=chat&agentId=${agentId}`, {
97
+ method: "POST",
98
+ headers: {
99
+ "X-Secret-Key": SECRET_KEY,
100
+ "Content-Type": "application/json",
101
+ },
102
+ body: JSON.stringify({
103
+ id: sid,
104
+ message: {
105
+ parts: [{ type: "text", text: message }],
106
+ },
107
+ }),
108
+ });
109
+
110
+ if (!res.ok) {
111
+ const err = await res.json().catch(() => ({}));
112
+ throw new Error((err as { error?: string }).error || `Chat error ${res.status}`);
113
+ }
114
+
115
+ // Streaming response — collect all text chunks
116
+ const text = await res.text();
117
+ // The SDK returns SSE-style data stream, extract text parts
118
+ const lines = text.split("\n");
119
+ const parts: string[] = [];
120
+ for (const line of lines) {
121
+ if (line.startsWith("0:")) {
122
+ // Vercel AI SDK text token format: 0:"text content"
123
+ try {
124
+ parts.push(JSON.parse(line.slice(2)));
125
+ } catch {
126
+ // skip malformed
127
+ }
128
+ }
129
+ }
130
+ return parts.join("") || text;
131
+ }
132
+
133
+ // --- Documents ---
134
+
135
+ export async function addDocument(
136
+ agentId: string,
137
+ title: string,
138
+ content: string
139
+ ) {
140
+ return sdkFetch("documents.create", {
141
+ method: "POST",
142
+ params: { agentId },
143
+ body: { title, content },
144
+ });
145
+ }
146
+
147
+ export async function listDocuments(agentId: string) {
148
+ return sdkFetch("documents.list", { params: { agentId } });
149
+ }
150
+
151
+ export async function getDocument(documentId: string) {
152
+ return sdkFetch("documents.get", { params: { documentId } });
153
+ }
154
+
155
+ export async function searchDocuments(agentId: string, query: string) {
156
+ return sdkFetch("documents.search", { params: { agentId, query } });
157
+ }
158
+
159
+ export async function updateDocument(
160
+ documentId: string,
161
+ data: { title?: string; content?: string }
162
+ ) {
163
+ return sdkFetch("documents.update", {
164
+ method: "PUT",
165
+ params: { documentId },
166
+ body: data,
167
+ });
168
+ }
169
+
170
+ export async function deleteDocument(documentId: string) {
171
+ return sdkFetch("documents.delete", {
172
+ method: "DELETE",
173
+ params: { documentId },
174
+ });
175
+ }
176
+
177
+ // --- Conversations ---
178
+
179
+ export async function listConversations(agentId: string) {
180
+ return sdkFetch("conversations.list", { params: { agentId } });
181
+ }
182
+
183
+ export async function getConversation(conversationId: string) {
184
+ return sdkFetch("conversations.get", { params: { conversationId } });
185
+ }
186
+
187
+ // --- Agent bridge mutations (NANOCLAW_WEBHOOK_SECRET auth) ---
188
+ // Pega a /api/v1/agents/conversations, no a /api/v2/sdk. Usa Bearer con el
189
+ // mismo secret que ya autentica el puente Formmy ↔ NanoClaw bidireccional,
190
+ // para no introducir env nuevas en el droplet de sofi-0.
191
+
192
+ const BRIDGE_SECRET = process.env.NANOCLAW_WEBHOOK_SECRET || "";
193
+
194
+ async function bridgeFetch(
195
+ intent: "set_estado" | "add_tag" | "remove_tag",
196
+ body: Record<string, unknown>,
197
+ ): Promise<unknown> {
198
+ if (!BRIDGE_SECRET) {
199
+ throw new Error("NANOCLAW_WEBHOOK_SECRET no está configurado en el entorno");
200
+ }
201
+ const url = new URL(`${API_URL}/api/v1/agents/conversations`);
202
+ url.searchParams.set("intent", intent);
203
+ const res = await fetch(url.toString(), {
204
+ method: "POST",
205
+ headers: {
206
+ Authorization: `Bearer ${BRIDGE_SECRET}`,
207
+ "Content-Type": "application/json",
208
+ },
209
+ body: JSON.stringify(body),
210
+ });
211
+ const data = await res.json().catch(() => ({}));
212
+ if (!res.ok) {
213
+ const msg = (data as { error?: string }).error || `Bridge error ${res.status}`;
214
+ throw new Error(msg);
215
+ }
216
+ return data;
217
+ }
218
+
219
+ export async function setConversationStatus(
220
+ conversationId: string,
221
+ label: string,
222
+ color: string,
223
+ ) {
224
+ return bridgeFetch("set_estado", { conversationId, label, color });
225
+ }
226
+
227
+ export async function addConversationTag(
228
+ conversationId: string,
229
+ label: string,
230
+ color: string,
231
+ comment?: string,
232
+ ) {
233
+ return bridgeFetch("add_tag", { conversationId, label, color, comment });
234
+ }
235
+
236
+ export async function removeConversationTag(
237
+ conversationId: string,
238
+ tagLabel: string,
239
+ ) {
240
+ return bridgeFetch("remove_tag", { conversationId, tagLabel });
241
+ }
242
+
243
+ // --- Sharing ---
244
+
245
+ export async function shareAgent(
246
+ agentId: string,
247
+ email: string,
248
+ role?: string
249
+ ) {
250
+ return sdkFetch("agents.share", {
251
+ method: "POST",
252
+ params: { agentId },
253
+ body: { email, role },
254
+ });
255
+ }
256
+
257
+ // --- Integrations ---
258
+
259
+ export async function connectIntegration(
260
+ agentId: string,
261
+ integration: string,
262
+ config: Record<string, string>
263
+ ) {
264
+ return sdkFetch("integrations.connect", {
265
+ method: "POST",
266
+ params: { agentId },
267
+ body: { integration, config },
268
+ });
269
+ }
@@ -0,0 +1,85 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import * as sdk from "../sdk-client.js";
4
+
5
+ export function registerAgentTools(server: McpServer) {
6
+ server.tool(
7
+ "list_agents",
8
+ "List all Formmy agents for the authenticated user",
9
+ {},
10
+ async () => {
11
+ const result = await sdk.listAgents();
12
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
13
+ }
14
+ );
15
+
16
+ server.tool(
17
+ "get_agent",
18
+ "Get details of a specific Formmy agent",
19
+ { agentId: z.string().describe("Agent ID or slug") },
20
+ async ({ agentId }) => {
21
+ const result = await sdk.getAgent(agentId);
22
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
23
+ }
24
+ );
25
+
26
+ server.tool(
27
+ "create_agent",
28
+ "Create a new Formmy agent/chatbot",
29
+ {
30
+ name: z.string().describe("Agent name"),
31
+ instructions: z.string().optional().describe("System prompt / instructions"),
32
+ welcomeMessage: z.string().optional().describe("Welcome message shown to users"),
33
+ model: z.string().optional().describe("AI model (e.g. gpt-4o-mini, gpt-4.1-mini, claude-3-5-haiku-latest)"),
34
+ },
35
+ async (params) => {
36
+ const result = await sdk.createAgent(params);
37
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38
+ }
39
+ );
40
+
41
+ server.tool(
42
+ "update_agent",
43
+ "Update an existing Formmy agent",
44
+ {
45
+ agentId: z.string().describe("Agent ID or slug"),
46
+ name: z.string().optional().describe("New name"),
47
+ instructions: z.string().optional().describe("New system prompt"),
48
+ customInstructions: z.string().optional().describe("Additional custom instructions"),
49
+ welcomeMessage: z.string().optional().describe("New welcome message"),
50
+ model: z.string().optional().describe("New AI model"),
51
+ },
52
+ async ({ agentId, ...data }) => {
53
+ const result = await sdk.updateAgent(agentId, data);
54
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
55
+ }
56
+ );
57
+
58
+ server.tool(
59
+ "share_agent",
60
+ "Share a Formmy agent with another user by email. Creates a permission so they can see and manage the agent.",
61
+ {
62
+ agentId: z.string().describe("Agent ID or slug"),
63
+ email: z.string().describe("Email of the user to share with"),
64
+ role: z.enum(["VIEWER", "EDITOR", "ADMIN"]).optional().describe("Role to grant (default: VIEWER)"),
65
+ },
66
+ async ({ agentId, email, role }) => {
67
+ const result = await sdk.shareAgent(agentId, email, role);
68
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
69
+ }
70
+ );
71
+
72
+ server.tool(
73
+ "connect_integration",
74
+ "Connect an external service (e.g. EasyBits DB) to a Formmy agent. The agent gets tools to interact with the service at chat time.",
75
+ {
76
+ agentId: z.string().describe("Agent ID or slug"),
77
+ integration: z.string().describe("Integration name (e.g. 'easybits')"),
78
+ config: z.record(z.string()).describe("Integration config (e.g. { apiKey, dbId })"),
79
+ },
80
+ async ({ agentId, integration, config }) => {
81
+ const result = await sdk.connectIntegration(agentId, integration, config);
82
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
83
+ }
84
+ );
85
+ }
@@ -0,0 +1,19 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import * as sdk from "../sdk-client.js";
4
+
5
+ export function registerChatTools(server: McpServer) {
6
+ server.tool(
7
+ "chat_with_agent",
8
+ "Send a message to a Formmy agent and get the full response (non-streaming)",
9
+ {
10
+ agentId: z.string().describe("Agent ID or slug"),
11
+ message: z.string().describe("Message to send"),
12
+ sessionId: z.string().optional().describe("Session ID for conversation continuity"),
13
+ },
14
+ async ({ agentId, message, sessionId }) => {
15
+ const response = await sdk.chatWithAgent(agentId, message, sessionId);
16
+ return { content: [{ type: "text", text: response }] };
17
+ }
18
+ );
19
+ }
@@ -0,0 +1,71 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import * as sdk from "../sdk-client.js";
4
+
5
+ export function registerConversationTools(server: McpServer) {
6
+ server.tool(
7
+ "list_conversations",
8
+ "List conversations for a Formmy agent",
9
+ {
10
+ agentId: z.string().describe("Agent ID or slug"),
11
+ },
12
+ async ({ agentId }) => {
13
+ const result = await sdk.listConversations(agentId);
14
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
15
+ }
16
+ );
17
+
18
+ server.tool(
19
+ "get_conversation",
20
+ "Get a conversation with its messages",
21
+ {
22
+ conversationId: z.string().describe("Conversation ID"),
23
+ },
24
+ async ({ conversationId }) => {
25
+ const result = await sdk.getConversation(conversationId);
26
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
27
+ }
28
+ );
29
+
30
+ server.tool(
31
+ "set_conversation_status",
32
+ "Set the CRM status (estado) of a conversation. Use when the user signals a clear state change: payment confirmed → 'Pago confirmado' (#10B981), needs human → 'Solo operador' (#3B82F6), in progress → 'Atendiendo' (#F59E0B), resolved → 'Atendido' (#10B981). label is free-form text; color must be a hex string. The operator sees the new chip in the conversations list immediately.",
33
+ {
34
+ conversationId: z.string().describe("Conversation ID"),
35
+ label: z.string().describe("Status label, e.g. 'Pago confirmado', 'Solo operador'"),
36
+ color: z.string().describe("Hex color, e.g. '#10B981' (green), '#3B82F6' (blue), '#F59E0B' (amber), '#EF4444' (red)"),
37
+ },
38
+ async ({ conversationId, label, color }) => {
39
+ const result = await sdk.setConversationStatus(conversationId, label, color);
40
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
41
+ }
42
+ );
43
+
44
+ server.tool(
45
+ "add_conversation_tag",
46
+ "Add a CRM tag to a conversation. Use to mark categorical attributes the operator should see at a glance: 'VIP', 'lead', 'urgente', 'cotización', etc. label is free-form; color is hex. Optional comment is internal note (not shown to the client). Returns the updated tag list.",
47
+ {
48
+ conversationId: z.string().describe("Conversation ID"),
49
+ label: z.string().describe("Tag label, e.g. 'VIP', 'lead', 'urgente'"),
50
+ color: z.string().describe("Hex color, e.g. '#A855F7' (purple/VIP), '#10B981' (green/lead), '#EF4444' (red/urgente)"),
51
+ comment: z.string().optional().describe("Optional internal note about why this tag was applied"),
52
+ },
53
+ async ({ conversationId, label, color, comment }) => {
54
+ const result = await sdk.addConversationTag(conversationId, label, color, comment);
55
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
56
+ }
57
+ );
58
+
59
+ server.tool(
60
+ "remove_conversation_tag",
61
+ "Remove a CRM tag from a conversation by its label. Case-insensitive match.",
62
+ {
63
+ conversationId: z.string().describe("Conversation ID"),
64
+ tagLabel: z.string().describe("Label of the tag to remove"),
65
+ },
66
+ async ({ conversationId, tagLabel }) => {
67
+ const result = await sdk.removeConversationTag(conversationId, tagLabel);
68
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
69
+ }
70
+ );
71
+ }
@@ -0,0 +1,82 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import * as sdk from "../sdk-client.js";
4
+
5
+ export function registerDocumentTools(server: McpServer) {
6
+ server.tool(
7
+ "add_document",
8
+ "Add a RAG document/knowledge base entry to a Formmy agent",
9
+ {
10
+ agentId: z.string().describe("Agent ID or slug"),
11
+ title: z.string().describe("Document title"),
12
+ content: z.string().describe("Document content (text, markdown, etc.)"),
13
+ },
14
+ async ({ agentId, title, content }) => {
15
+ const result = await sdk.addDocument(agentId, title, content);
16
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
17
+ }
18
+ );
19
+
20
+ server.tool(
21
+ "list_documents",
22
+ "List all RAG documents for a Formmy agent",
23
+ {
24
+ agentId: z.string().describe("Agent ID or slug"),
25
+ },
26
+ async ({ agentId }) => {
27
+ const result = await sdk.listDocuments(agentId);
28
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
29
+ }
30
+ );
31
+
32
+ server.tool(
33
+ "get_document",
34
+ "Get a specific RAG document with its full content",
35
+ {
36
+ documentId: z.string().describe("Document ID"),
37
+ },
38
+ async ({ documentId }) => {
39
+ const result = await sdk.getDocument(documentId);
40
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
41
+ }
42
+ );
43
+
44
+ server.tool(
45
+ "search_documents",
46
+ "Semantic search across an agent's RAG documents",
47
+ {
48
+ agentId: z.string().describe("Agent ID or slug"),
49
+ query: z.string().describe("Search query"),
50
+ },
51
+ async ({ agentId, query }) => {
52
+ const result = await sdk.searchDocuments(agentId, query);
53
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
54
+ }
55
+ );
56
+
57
+ server.tool(
58
+ "update_document",
59
+ "Update an existing RAG document's title or content",
60
+ {
61
+ documentId: z.string().describe("Document ID"),
62
+ title: z.string().optional().describe("New title"),
63
+ content: z.string().optional().describe("New content"),
64
+ },
65
+ async ({ documentId, ...data }) => {
66
+ const result = await sdk.updateDocument(documentId, data);
67
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
68
+ }
69
+ );
70
+
71
+ server.tool(
72
+ "delete_document",
73
+ "Delete a RAG document from an agent",
74
+ {
75
+ documentId: z.string().describe("Document ID"),
76
+ },
77
+ async ({ documentId }) => {
78
+ const result = await sdk.deleteDocument(documentId);
79
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
80
+ }
81
+ );
82
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "declaration": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["dist", "node_modules"]
17
+ }