From be1a14f78315a1a21e1a2d6068f3a73587d767f1 Mon Sep 17 00:00:00 2001 From: Garfield Date: Fri, 15 May 2026 10:49:18 -0400 Subject: [PATCH] feat(chat): upgrade to full agentic demo bot (Option B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat widget now runs a live tool-use loop via Claude Haiku. Exposes slack, discord, and telegram demo tools — bot can actually send messages and read channels to prove the platform works in real time. Widget shows a purple pill with tool names when the agent calls a live platform. Co-Authored-By: Claude Sonnet 4.6 --- product/chat-widget.js | 11 ++++ src/chat.ts | 122 +++++++++++++++++++++++++++++++++-------- src/index.ts | 4 +- 3 files changed, 111 insertions(+), 26 deletions(-) diff --git a/product/chat-widget.js b/product/chat-widget.js index b8dfa77..d40119b 100644 --- a/product/chat-widget.js +++ b/product/chat-widget.js @@ -50,6 +50,11 @@ background: ${BRAND}; color: #fff; border-bottom-right-radius: 4px; align-self: flex-end; } .smcp-msg.typing { color: #888; font-style: italic; } + .smcp-tools-used { + font-size: 11px; color: #6c47ff; background: #f1f0fe; + border-radius: 6px; padding: 4px 8px; align-self: flex-start; + display: flex; align-items: center; gap: 5px; + } #smcp-input-row { padding: 10px 12px; border-top: 1px solid #eee; display: flex; gap: 8px; flex-shrink: 0; @@ -152,6 +157,12 @@ indicator.textContent = reply; indicator.classList.remove('typing'); history.push({ role: 'assistant', content: reply }); + if (data.toolsUsed && data.toolsUsed.length > 0) { + const pill = document.createElement('div'); + pill.className = 'smcp-tools-used'; + pill.textContent = '⚡ Live: ' + data.toolsUsed.join(', '); + messages.appendChild(pill); + } } catch { indicator.textContent = 'Network error. Please try again.'; indicator.classList.remove('typing'); diff --git a/src/chat.ts b/src/chat.ts index 74f1e31..1dfd5cd 100644 --- a/src/chat.ts +++ b/src/chat.ts @@ -1,49 +1,123 @@ import Anthropic from '@anthropic-ai/sdk'; +import { tools as allTools, handleToolCall } from './tools.js'; const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); -const SYSTEM_PROMPT = `You are a friendly and knowledgeable support assistant for SquareMCP — an AI Social Media Gateway that lets AI agents (Claude, ChatGPT, Codex CLI, etc.) post to social platforms via the Model Context Protocol (MCP). +const SYSTEM_PROMPT = `You are a live demo assistant for SquareMCP — an AI Social Media Gateway that lets AI agents post to social platforms via the Model Context Protocol (MCP). + +You have real tools connected to live platforms. When a visitor asks "can it send a Slack message?" or "show me how it works" — actually do it. Send a demo message, read channels, show real results. What SquareMCP does: - Connects AI coding assistants to social platforms: LinkedIn, TikTok, WhatsApp, Instagram, Twitter/X, Facebook, Telegram, Discord, Slack, and Email - Works with any MCP-compatible client: Claude Desktop, Claude Code, Cursor, Windsurf, opencode, Codex CLI - Provides a multi-tenant SaaS platform where each customer securely stores their own platform credentials -- Offers a simple dashboard to connect platforms, view usage, and manage webhooks - Plans: Free (100 calls/month), Pro, Business -How it works: -1. Customer signs up at the SquareMCP dashboard -2. Connects their social accounts (bot tokens, API keys) -3. Adds the SquareMCP MCP server to their AI client config -4. Their AI agent can now send messages, post content, read analytics — on any connected platform +When using tools: +- For demo sends (Slack, Telegram, Discord), send a short, friendly message that explains this is a live SquareMCP demo +- After a tool succeeds, explain what just happened and how easy it was +- If a tool fails (missing credentials), explain what credential the customer would set up instead +- Never expose tokens or internal IDs in your reply -Common questions: -- Pricing: Free tier available, paid plans for higher usage -- Supported platforms: LinkedIn, TikTok, WhatsApp Business, Instagram, Twitter/X, Facebook, Telegram, Discord, Slack, Email -- No coding required to use the dashboard; coding experience helps to get the most from MCP tool calls -- Webhooks: customers can receive real-time events when messages arrive -- Security: credentials are encrypted at rest, each customer's data is isolated +Keep replies concise. If you don't know something, say so and suggest emailing support@squaremcp.com.`; -Keep answers concise (2-4 sentences max). If you don't know something, say so and suggest emailing support@squaremcp.com. Never make up features or pricing. Speak in a warm, direct tone.`; +// Tools the public demo agent is allowed to use (no customer auth needed — uses env var creds) +const DEMO_TOOL_NAMES = new Set([ + 'slack_get_me', + 'slack_get_channels', + 'slack_send_message', + 'slack_get_messages', + 'discord_get_me', + 'discord_get_guilds', + 'discord_send_message', + 'telegram_get_me', + 'telegram_send_message', +]); + +const demoTools = allTools + .filter(t => DEMO_TOOL_NAMES.has(t.name)) + .map(t => ({ + name: t.name, + description: t.description, + input_schema: t.inputSchema as Anthropic.Tool['input_schema'], + })); export interface ChatMessage { role: 'user' | 'assistant'; content: string; } -export async function handleChat(messages: ChatMessage[]): Promise { +export interface ChatResult { + reply: string; + toolsUsed: string[]; +} + +export async function handleChat(messages: ChatMessage[]): Promise { if (!process.env.ANTHROPIC_API_KEY) { throw new Error('ANTHROPIC_API_KEY not configured'); } - const response = await client.messages.create({ - model: 'claude-haiku-4-5-20251001', - max_tokens: 512, - system: SYSTEM_PROMPT, - messages: messages.map(m => ({ role: m.role, content: m.content })), - }); + const apiMessages: Anthropic.MessageParam[] = messages.map(m => ({ + role: m.role, + content: m.content, + })); - const block = response.content[0]; - if (block.type !== 'text') throw new Error('Unexpected response type'); - return block.text; + const toolsUsed: string[] = []; + const MAX_ITERATIONS = 5; + + for (let i = 0; i < MAX_ITERATIONS; i++) { + const response = await client.messages.create({ + model: 'claude-haiku-4-5-20251001', + max_tokens: 1024, + system: SYSTEM_PROMPT, + tools: demoTools, + messages: apiMessages, + }); + + if (response.stop_reason === 'end_turn') { + const text = response.content.find(b => b.type === 'text'); + return { reply: text?.text ?? 'Done.', toolsUsed }; + } + + if (response.stop_reason === 'tool_use') { + // Add Claude's response (with tool_use blocks) to the message history + apiMessages.push({ role: 'assistant', content: response.content }); + + // Execute each tool call and collect results + const toolResults: Anthropic.ToolResultBlockParam[] = []; + for (const block of response.content) { + if (block.type !== 'tool_use') continue; + toolsUsed.push(block.name); + try { + const result = await handleToolCall( + block.name, + block.input as Record, + undefined // no customer — uses env var credentials + ); + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: result.content[0]?.text ?? '', + is_error: result.isError, + }); + } catch (err) { + toolResults.push({ + type: 'tool_result', + tool_use_id: block.id, + content: `Error: ${(err as Error).message}`, + is_error: true, + }); + } + } + + apiMessages.push({ role: 'user', content: toolResults }); + continue; + } + + // Unexpected stop reason — return whatever text we have + const text = response.content.find(b => b.type === 'text'); + return { reply: text?.text ?? 'Something unexpected happened.', toolsUsed }; + } + + return { reply: 'I ran too many steps. Please try a simpler request.', toolsUsed }; } diff --git a/src/index.ts b/src/index.ts index 1babe85..dfdc026 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1988,8 +1988,8 @@ app.post('/api/chat', async (req, res) => { return; } try { - const reply = await handleChat(messages); - res.json({ reply }); + const { reply, toolsUsed } = await handleChat(messages); + res.json({ reply, toolsUsed }); } catch (err) { console.error('[chat] error:', (err as Error).message); res.status(500).json({ error: 'Chat unavailable' });