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:
@@ -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');
|
||||
|
||||
122
src/chat.ts
122
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<string> {
|
||||
export interface ChatResult {
|
||||
reply: string;
|
||||
toolsUsed: string[];
|
||||
}
|
||||
|
||||
export async function handleChat(messages: ChatMessage[]): Promise<ChatResult> {
|
||||
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<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 };
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user