Steps 0–10 of the v2 plan, 194 tests passing. Core infrastructure - Shared Redis client (src/redis.ts); all four Redis consumers migrated - Vitest test harness with vitest.config.ts and npm test/test:watch scripts Billing & invoicing (Steps 1–2) - Monthly invoice generation with idempotency (MySQL uq_customer_period unique key) - Cron job with Redis distributed lock (Lua compare-delete, 1-hr TTL) - Invoice emailer via nodemailer (FETCHERPAY SMTP) - Billing middleware: checkLimit gate in handleToolCall; platform attribution fix Email multi-tenancy (Step 3) - EmailCtx = Account | EmailCredentials; imap.ts + smtp.ts accept both - resolveEmailCtx helper in tools.ts; all email tools use customer credentials Analytics + platform health (Steps 4–5) - Chart.js bar charts for platform breakdown and daily activity - Token expiry check in getCredential with dynamic import refresh - platform-health.ts: per-platform health probe with 10-min Redis cache - GET /api/health/platforms; "Token expired" amber badge in dashboard Tool schema filtering (Step 6) - stripAccountParam deep-clones tool schemas; multi-tenant sessions never see the internal account enum OAuth hardening (Step 7) - Atomic auth code consumption: UPDATE SET used=TRUE, check affectedRows - customer_id threaded through oauth_auth_codes → oauth_tokens - getTokenCustomer(); requireAuth resolves req.customer from Bearer token - Consent page requires authenticated session; redirect_uri validated against registered URIs; http://localhost:* loopback wildcard DCR browser flow (Step 8) - ensureOAuthAppRegistered() upserts pre-registered SquareMCP OAuth app on startup with redirect URIs for mcp-callback, localhost:*, claude-desktop, opencode - GET /oauth/connect-mcp → server-side redirect (client_id off frontend) - GET /oauth/mcp-callback → exchanges code, renders config snippet page with copy buttons for Claude Desktop and Codex CLI Webhooks (Step 9) - webhook_url + webhook_secret columns on customers - deliverWebhook(): HMAC-SHA256 signing, 3× exponential retry (1s/4s/16s), Redis DLQ with 7-day TTL on total failure - isValidWebhookUrl(): SSRF protection (blocks RFC-1918, localhost, .local) - POST /api/webhooks/config (secret returned once), GET, DELETE - GET /api/admin/webhooks/dlq/:customerId - WhatsApp POST route uses express.raw() for raw body preservation - Dashboard Webhooks tab with secret-once display and copy button Developer docs (Step 10) - docs/ static HTML site (GitHub Pages, no build pipeline) - index.html: landing page with client + platform overview - getting-started.html: tabbed MCP config for Claude Desktop, Codex CLI, opencode - platforms.html: LinkedIn, TikTok, WhatsApp, Instagram, Twitter, Telegram guides - agent-tutorial.html: complete Node.js agent (Anthropic SDK + MCP SDK), LinkedIn posting loop, extensions for multi-platform + inbound webhook reaction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
232 lines
12 KiB
HTML
232 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Agent Tutorial — SquareMCP Docs</title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="site-nav">
|
|
<div class="nav-inner">
|
|
<a href="index.html" class="nav-logo"><span class="nav-logo-mark">S</span> SquareMCP</a>
|
|
<div class="nav-links">
|
|
<a href="getting-started.html">Getting started</a>
|
|
<a href="platforms.html">Platform guides</a>
|
|
<a href="agent-tutorial.html" class="active">Agent tutorial</a>
|
|
<a href="https://hermes.squaremcp.com/openapi-social.json" target="_blank">API reference ↗</a>
|
|
</div>
|
|
<a href="https://squaremcp.com" class="nav-cta" target="_blank">Open app</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="page">
|
|
<div class="hero">
|
|
<h1>Build a LinkedIn posting agent</h1>
|
|
<p>Real code: a Claude agent that researches a topic, drafts a post, and publishes it to LinkedIn — fully automated.</p>
|
|
</div>
|
|
|
|
<div class="callout">
|
|
<strong>What you'll build</strong>
|
|
A Node.js script that (1) takes a topic from the command line, (2) uses Claude to research and draft a LinkedIn post, (3) calls the SquareMCP <code>linkedin_create_post</code> tool to publish — all in one agentic loop.
|
|
</div>
|
|
|
|
<h2>Prerequisites</h2>
|
|
<ul>
|
|
<li>Node.js 18+ with ESM support</li>
|
|
<li>An <a href="https://console.anthropic.com" target="_blank">Anthropic API key</a></li>
|
|
<li>A SquareMCP Bearer token with LinkedIn connected (see <a href="getting-started.html">Getting started</a>)</li>
|
|
</ul>
|
|
|
|
<h2>Step 1 — Install dependencies</h2>
|
|
<pre><code>npm init -y
|
|
npm install @anthropic-ai/sdk @modelcontextprotocol/sdk</code></pre>
|
|
|
|
<h2>Step 2 — Create the agent</h2>
|
|
|
|
<p>Create <code>linkedin-agent.mjs</code>:</p>
|
|
|
|
<pre><code><span class="kw">import</span> Anthropic <span class="kw">from</span> <span class="str">'@anthropic-ai/sdk'</span>;
|
|
<span class="kw">import</span> { Client } <span class="kw">from</span> <span class="str">'@modelcontextprotocol/sdk/client/index.js'</span>;
|
|
<span class="kw">import</span> { StreamableHTTPClientTransport } <span class="kw">from</span> <span class="str">'@modelcontextprotocol/sdk/client/streamableHttp.js'</span>;
|
|
|
|
<span class="kw">const</span> SQUAREMCP_URL = <span class="str">'https://hermes.squaremcp.com/mcp'</span>;
|
|
<span class="kw">const</span> SQUAREMCP_TOKEN = process.env.<span class="cls">SQUAREMCP_TOKEN</span>; <span class="cmt">// your Bearer token</span>
|
|
<span class="kw">const</span> topic = process.argv[<span class="num">2</span>] ?? <span class="str">'AI trends in 2026'</span>;
|
|
|
|
<span class="cmt">// ── 1. Connect to SquareMCP ──────────────────────────────────────</span>
|
|
<span class="kw">const</span> transport = <span class="kw">new</span> <span class="cls">StreamableHTTPClientTransport</span>(<span class="kw">new</span> <span class="cls">URL</span>(SQUAREMCP_URL), {
|
|
requestInit: { headers: { Authorization: <span class="str">`Bearer <span class="kw">${</span>SQUAREMCP_TOKEN<span class="kw">}</span>`</span> } },
|
|
});
|
|
|
|
<span class="kw">const</span> mcpClient = <span class="kw">new</span> <span class="cls">Client</span>({ name: <span class="str">'linkedin-agent'</span>, version: <span class="str">'1.0.0'</span> });
|
|
<span class="kw">await</span> mcpClient.connect(transport);
|
|
|
|
<span class="cmt">// Fetch available tools from SquareMCP</span>
|
|
<span class="kw">const</span> { tools: mcpTools } = <span class="kw">await</span> mcpClient.<span class="fn">listTools</span>();
|
|
|
|
<span class="cmt">// Convert MCP tool descriptors to Anthropic tool format</span>
|
|
<span class="kw">const</span> anthropicTools = mcpTools.<span class="fn">map</span>(t => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
input_schema: t.inputSchema,
|
|
}));
|
|
|
|
<span class="cmt">// ── 2. Run the agentic loop ──────────────────────────────────────</span>
|
|
<span class="kw">const</span> anthropic = <span class="kw">new</span> <span class="cls">Anthropic</span>();
|
|
<span class="kw">const</span> messages = [
|
|
{
|
|
role: <span class="str">'user'</span>,
|
|
content: <span class="str">`You are a LinkedIn content strategist. Your job:
|
|
1. Think about the topic: "<span class="kw">${</span>topic<span class="kw">}</span>"
|
|
2. Draft a compelling LinkedIn post (150-250 words, professional tone, 3-5 hashtags)
|
|
3. Call linkedin_create_post to publish it
|
|
4. Report back what was posted.
|
|
|
|
Write the post now and publish it.`</span>,
|
|
},
|
|
];
|
|
|
|
console.<span class="fn">log</span>(<span class="str">`\n🤖 Agent starting — topic: "<span class="kw">${</span>topic<span class="kw">}</span>"\n`</span>);
|
|
|
|
<span class="kw">while</span> (<span class="kw">true</span>) {
|
|
<span class="kw">const</span> response = <span class="kw">await</span> anthropic.messages.<span class="fn">create</span>({
|
|
model: <span class="str">'claude-opus-4-7'</span>,
|
|
max_tokens: <span class="num">1024</span>,
|
|
tools: anthropicTools,
|
|
messages,
|
|
});
|
|
|
|
<span class="kmt">// Append assistant turn</span>
|
|
messages.<span class="fn">push</span>({ role: <span class="str">'assistant'</span>, content: response.content });
|
|
|
|
<span class="kw">if</span> (response.stop_reason === <span class="str">'end_turn'</span>) {
|
|
<span class="kw">const</span> text = response.content
|
|
.<span class="fn">filter</span>(b => b.type === <span class="str">'text'</span>)
|
|
.<span class="fn">map</span>(b => b.text)
|
|
.<span class="fn">join</span>(<span class="str">'\n'</span>);
|
|
console.<span class="fn">log</span>(<span class="str">'\n✅ Agent finished:\n'</span>, text);
|
|
<span class="kw">break</span>;
|
|
}
|
|
|
|
<span class="kw">if</span> (response.stop_reason !== <span class="str">'tool_use'</span>) <span class="kw">break</span>;
|
|
|
|
<span class="cmt">// Execute each tool call against SquareMCP</span>
|
|
<span class="kw">const</span> toolResults = [];
|
|
<span class="kw">for</span> (<span class="kw">const</span> block <span class="kw">of</span> response.content) {
|
|
<span class="kw">if</span> (block.type !== <span class="str">'tool_use'</span>) <span class="kw">continue</span>;
|
|
|
|
console.<span class="fn">log</span>(<span class="str">`🔧 Calling <span class="kw">${</span>block.name<span class="kw">}</span>...`</span>);
|
|
<span class="kw">let</span> result;
|
|
<span class="kw">try</span> {
|
|
result = <span class="kw">await</span> mcpClient.<span class="fn">callTool</span>({ name: block.name, arguments: block.input });
|
|
console.<span class="fn">log</span>(<span class="str">` ✓ <span class="kw">${</span>JSON.<span class="fn">stringify</span>(result.content[<span class="num">0</span>]).<span class="fn">slice</span>(<span class="num">0</span>, <span class="num">120</span>)<span class="kw">}</span>...`</span>);
|
|
} <span class="kw">catch</span> (err) {
|
|
result = { content: [{ type: <span class="str">'text'</span>, text: <span class="str">`Error: <span class="kw">${</span>err.message<span class="kw">}</span>`</span> }] };
|
|
console.<span class="fn">error</span>(<span class="str">` ✗ <span class="kw">${</span>err.message<span class="kw">}</span>`</span>);
|
|
}
|
|
|
|
toolResults.<span class="fn">push</span>({
|
|
type: <span class="str">'tool_result'</span>,
|
|
tool_use_id: block.id,
|
|
content: result.content,
|
|
});
|
|
}
|
|
|
|
messages.<span class="fn">push</span>({ role: <span class="str">'user'</span>, content: toolResults });
|
|
}
|
|
|
|
<span class="kw">await</span> mcpClient.<span class="fn">close</span>();</code></pre>
|
|
|
|
<h2>Step 3 — Run it</h2>
|
|
|
|
<pre><code>export ANTHROPIC_API_KEY=sk-ant-...
|
|
export SQUAREMCP_TOKEN=your-bearer-token-here
|
|
|
|
node linkedin-agent.mjs "The future of AI agents in enterprise software"</code></pre>
|
|
|
|
<p>Expected output:</p>
|
|
|
|
<pre><code>🤖 Agent starting — topic: "The future of AI agents in enterprise software"
|
|
|
|
🔧 Calling linkedin_create_post...
|
|
✓ {"id":"urn:li:share:7194...","success":true}...
|
|
|
|
✅ Agent finished:
|
|
I've published the following LinkedIn post:
|
|
|
|
"Enterprise software is undergoing a quiet revolution..."
|
|
[full post text]
|
|
|
|
The post has been published successfully to your LinkedIn feed.</code></pre>
|
|
|
|
<h2>Step 4 — Extend it</h2>
|
|
|
|
<p>Now that you have the agentic loop working, you can extend it:</p>
|
|
|
|
<h3>Post to multiple platforms at once</h3>
|
|
<pre><code><span class="cmt">// Replace the user message with:</span>
|
|
content: <span class="str">`Draft a post about "<span class="kw">${</span>topic<span class="kw">}</span>" and publish it on both
|
|
LinkedIn (professional tone) and Twitter (punchy, max 280 chars, 2 hashtags).
|
|
Use linkedin_create_post and twitter_create_tweet.`</span></code></pre>
|
|
|
|
<h3>Schedule with a cron job</h3>
|
|
<pre><code><span class="cmt"># Post every weekday at 9am</span>
|
|
0 9 * * 1-5 SQUAREMCP_TOKEN=... ANTHROPIC_API_KEY=... \
|
|
node /path/to/linkedin-agent.mjs "$(cat /path/to/topics.txt | shuf -n1)"</code></pre>
|
|
|
|
<h3>React to inbound WhatsApp messages</h3>
|
|
<p>Configure a webhook in the SquareMCP dashboard. When a WhatsApp message arrives, SquareMCP POSTs to your server. Run the same agent loop but start with the inbound message as context:</p>
|
|
|
|
<pre><code><span class="kw">import</span> express <span class="kw">from</span> <span class="str">'express'</span>;
|
|
<span class="kw">import</span> crypto <span class="kw">from</span> <span class="str">'crypto'</span>;
|
|
|
|
<span class="kw">const</span> app = <span class="fn">express</span>();
|
|
app.<span class="fn">use</span>(express.<span class="fn">json</span>());
|
|
|
|
app.<span class="fn">post</span>(<span class="str">'/webhook'</span>, <span class="kw">async</span> (req, res) => {
|
|
<span class="cmt">// Verify signature</span>
|
|
<span class="kw">const</span> sig = req.headers[<span class="str">'x-squaremcp-signature'</span>];
|
|
<span class="kw">const</span> expected = <span class="str">`sha256=<span class="kw">${</span>crypto
|
|
.<span class="fn">createHmac</span>(<span class="str">'sha256'</span>, process.env.<span class="cls">WEBHOOK_SECRET</span>)
|
|
.<span class="fn">update</span>(JSON.<span class="fn">stringify</span>(req.body))
|
|
.<span class="fn">digest</span>(<span class="str">'hex'</span>)<span class="kw">}</span>`</span>;
|
|
<span class="kw">if</span> (sig !== expected) { res.<span class="fn">status</span>(<span class="num">401</span>).<span class="fn">end</span>(); <span class="kw">return</span>; }
|
|
|
|
res.<span class="fn">status</span>(<span class="num">200</span>).<span class="fn">end</span>(); <span class="cmt">// acknowledge immediately</span>
|
|
|
|
<span class="kw">const</span> { platform, data } = req.body;
|
|
console.<span class="fn">log</span>(<span class="str">`Inbound from <span class="kw">${</span>platform<span class="kw">}</span>: <span class="kw">${</span>data.text<span class="kw">}</span>`</span>);
|
|
|
|
<span class="cmt">// Run agent in background...</span>
|
|
<span class="fn">runAgent</span>(data).catch(console.error);
|
|
});
|
|
|
|
app.<span class="fn">listen</span>(<span class="num">3000</span>);</code></pre>
|
|
|
|
<h2>Going further</h2>
|
|
|
|
<div class="card-grid">
|
|
<a href="getting-started.html" class="card" style="text-decoration:none;color:inherit;">
|
|
<h4>← Getting started</h4>
|
|
<p>Configure Claude Desktop, Codex CLI, or opencode.</p>
|
|
</a>
|
|
<a href="platforms.html" class="card" style="text-decoration:none;color:inherit;">
|
|
<h4>Platform guides</h4>
|
|
<p>Connect TikTok, WhatsApp, Instagram, and more.</p>
|
|
</a>
|
|
<a href="https://hermes.squaremcp.com/openapi-social.json" target="_blank" class="card" style="text-decoration:none;color:inherit;">
|
|
<h4>API reference ↗</h4>
|
|
<p>Full tool schemas for every platform.</p>
|
|
</a>
|
|
<a href="https://docs.anthropic.com/en/api/tool-use" target="_blank" class="card" style="text-decoration:none;color:inherit;">
|
|
<h4>Anthropic tool use ↗</h4>
|
|
<p>Deep dive into Claude's tool-use API.</p>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|