feat(chat): upgrade to full agentic demo bot (Option B)

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 <noreply@anthropic.com>
This commit is contained in:
Garfield
2026-05-15 10:49:18 -04:00
parent 4bf93d6763
commit be1a14f783
3 changed files with 111 additions and 26 deletions

View File

@@ -50,6 +50,11 @@
background: ${BRAND}; color: #fff; border-bottom-right-radius: 4px; align-self: flex-end; background: ${BRAND}; color: #fff; border-bottom-right-radius: 4px; align-self: flex-end;
} }
.smcp-msg.typing { color: #888; font-style: italic; } .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 { #smcp-input-row {
padding: 10px 12px; border-top: 1px solid #eee; padding: 10px 12px; border-top: 1px solid #eee;
display: flex; gap: 8px; flex-shrink: 0; display: flex; gap: 8px; flex-shrink: 0;
@@ -152,6 +157,12 @@
indicator.textContent = reply; indicator.textContent = reply;
indicator.classList.remove('typing'); indicator.classList.remove('typing');
history.push({ role: 'assistant', content: reply }); 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 { } catch {
indicator.textContent = 'Network error. Please try again.'; indicator.textContent = 'Network error. Please try again.';
indicator.classList.remove('typing'); indicator.classList.remove('typing');

View File

@@ -1,49 +1,123 @@
import Anthropic from '@anthropic-ai/sdk'; 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 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: What SquareMCP does:
- Connects AI coding assistants to social platforms: LinkedIn, TikTok, WhatsApp, Instagram, Twitter/X, Facebook, Telegram, Discord, Slack, and Email - 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 - 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 - 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 - Plans: Free (100 calls/month), Pro, Business
How it works: When using tools:
1. Customer signs up at the SquareMCP dashboard - For demo sends (Slack, Telegram, Discord), send a short, friendly message that explains this is a live SquareMCP demo
2. Connects their social accounts (bot tokens, API keys) - After a tool succeeds, explain what just happened and how easy it was
3. Adds the SquareMCP MCP server to their AI client config - If a tool fails (missing credentials), explain what credential the customer would set up instead
4. Their AI agent can now send messages, post content, read analytics — on any connected platform - Never expose tokens or internal IDs in your reply
Common questions: Keep replies concise. If you don't know something, say so and suggest emailing support@squaremcp.com.`;
- 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 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 { export interface ChatMessage {
role: 'user' | 'assistant'; role: 'user' | 'assistant';
content: string; content: string;
} }
export async function handleChat(messages: ChatMessage[]): Promise<string> { export interface ChatResult {
reply: string;
toolsUsed: string[];
}
export async function handleChat(messages: ChatMessage[]): Promise<ChatResult> {
if (!process.env.ANTHROPIC_API_KEY) { if (!process.env.ANTHROPIC_API_KEY) {
throw new Error('ANTHROPIC_API_KEY not configured'); throw new Error('ANTHROPIC_API_KEY not configured');
} }
const apiMessages: Anthropic.MessageParam[] = messages.map(m => ({
role: m.role,
content: m.content,
}));
const toolsUsed: string[] = [];
const MAX_ITERATIONS = 5;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await client.messages.create({ const response = await client.messages.create({
model: 'claude-haiku-4-5-20251001', model: 'claude-haiku-4-5-20251001',
max_tokens: 512, max_tokens: 1024,
system: SYSTEM_PROMPT, system: SYSTEM_PROMPT,
messages: messages.map(m => ({ role: m.role, content: m.content })), tools: demoTools,
messages: apiMessages,
}); });
const block = response.content[0]; if (response.stop_reason === 'end_turn') {
if (block.type !== 'text') throw new Error('Unexpected response type'); const text = response.content.find(b => b.type === 'text');
return block.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<string, unknown>,
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 };
} }

View File

@@ -1988,8 +1988,8 @@ app.post('/api/chat', async (req, res) => {
return; return;
} }
try { try {
const reply = await handleChat(messages); const { reply, toolsUsed } = await handleChat(messages);
res.json({ reply }); res.json({ reply, toolsUsed });
} catch (err) { } catch (err) {
console.error('[chat] error:', (err as Error).message); console.error('[chat] error:', (err as Error).message);
res.status(500).json({ error: 'Chat unavailable' }); res.status(500).json({ error: 'Chat unavailable' });