feat: Slack platform + Claude-powered chat support widget
- Add Slack as customer-facing messaging platform (client, 4 MCP tools, dashboard card) - Add /api/chat endpoint powered by Claude Haiku with SquareMCP system prompt - Add embeddable chat-widget.js injected into all 3 sites (docs, app, www) - Add ANTHROPIC_API_KEY, serve product/ as static files - Update Platform type to include slack Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -227,5 +227,6 @@ app.<span class="fn">listen</span>(<span class="num">3000</span>);</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -208,5 +208,6 @@ function switchTab(btn, id) {
|
|||||||
document.getElementById('tab-' + id).classList.add('active');
|
document.getElementById('tab-' + id).classList.add('active');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -90,5 +90,6 @@ document.querySelectorAll('.nav-links a').forEach(a => {
|
|||||||
if (a.href === location.href) a.classList.add('active');
|
if (a.href === location.href) a.classList.add('active');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -284,5 +284,6 @@ Search Twitter for tweets mentioning "SquareMCP"</code></pre>
|
|||||||
Get the latest Telegram messages in my channel</code></pre>
|
Get the latest Telegram messages in my channel</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
72
package-lock.json
generated
72
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "hermes-mcp",
|
"name": "hermes-mcp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.96.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@@ -39,6 +40,27 @@
|
|||||||
"vitest": "^4.1.6"
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
|
"version": "0.96.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.96.0.tgz",
|
||||||
|
"integrity": "sha512-KlCsODtTyb17bLUVCSDC2HtSvAbJf60sEiPEax9dInF+aDF92vS4TZJ5XD7YCQXNb1/5icYaw8Y7wMjPlIV9Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema-to-ts": "^3.1.1",
|
||||||
|
"standardwebhooks": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"anthropic-ai-sdk": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
@@ -75,6 +97,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||||
|
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
@@ -1334,6 +1365,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
@@ -2436,6 +2473,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
@@ -2873,6 +2916,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-to-ts": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"ts-algebra": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
@@ -4241,6 +4297,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -4419,6 +4485,12 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-algebra": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.96.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
|||||||
@@ -103,6 +103,15 @@ const PLATFORM_CONFIG = {
|
|||||||
],
|
],
|
||||||
help: 'From Meta Business Settings → WhatsApp → API Setup. You need a verified business phone number.',
|
help: 'From Meta Business Settings → WhatsApp → API Setup. You need a verified business phone number.',
|
||||||
},
|
},
|
||||||
|
slack: {
|
||||||
|
name: 'Slack',
|
||||||
|
type: 'token',
|
||||||
|
fields: [
|
||||||
|
{ key: 'accessToken', label: 'Bot Token', type: 'password' },
|
||||||
|
{ key: 'channelId', label: 'Default Channel ID', type: 'text' },
|
||||||
|
],
|
||||||
|
help: 'Go to api.slack.com/apps → Create App → OAuth & Permissions → install to workspace. Copy the Bot User OAuth Token.',
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
name: 'Email',
|
name: 'Email',
|
||||||
type: 'token',
|
type: 'token',
|
||||||
@@ -403,7 +412,7 @@ async function loadInvoices() {
|
|||||||
const PLATFORM_COLORS = {
|
const PLATFORM_COLORS = {
|
||||||
email: '#ea4335', linkedin: '#0a66c2', twitter: '#000000',
|
email: '#ea4335', linkedin: '#0a66c2', twitter: '#000000',
|
||||||
instagram: '#e1306c', facebook: '#1877f2', tiktok: '#010101',
|
instagram: '#e1306c', facebook: '#1877f2', tiktok: '#010101',
|
||||||
whatsapp: '#25d366', telegram: '#0088cc', discord: '#5865f2',
|
whatsapp: '#25d366', telegram: '#0088cc', discord: '#5865f2', slack: '#4a154b',
|
||||||
snapchat: '#fffc00', obsidian: '#7c3aed',
|
snapchat: '#fffc00', obsidian: '#7c3aed',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,17 @@
|
|||||||
<button class="btn btn-connect" data-platform="whatsapp">Connect</button>
|
<button class="btn btn-connect" data-platform="whatsapp">Connect</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Slack -->
|
||||||
|
<div class="platform-card" data-platform="slack">
|
||||||
|
<div class="platform-icon" style="background:#4a154b;">💬</div>
|
||||||
|
<div class="platform-info">
|
||||||
|
<h3>Slack</h3>
|
||||||
|
<p class="platform-desc">Send messages to channels</p>
|
||||||
|
<span class="status-badge disconnected">Not connected</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-connect" data-platform="slack">Connect</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="platform-card" data-platform="email">
|
<div class="platform-card" data-platform="email">
|
||||||
<div class="platform-icon" style="background:#ea4335;">✉️</div>
|
<div class="platform-icon" style="background:#ea4335;">✉️</div>
|
||||||
@@ -271,5 +282,6 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
180
product/chat-widget.js
Normal file
180
product/chat-widget.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const API = 'https://hermes.squaremcp.com/api/chat';
|
||||||
|
const BRAND = '#6c47ff';
|
||||||
|
const BRAND_DARK = '#5535e0';
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
#smcp-btn {
|
||||||
|
position: fixed; bottom: 24px; right: 24px; z-index: 9998;
|
||||||
|
width: 56px; height: 56px; border-radius: 50%;
|
||||||
|
background: ${BRAND}; border: none; cursor: pointer;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#smcp-btn:hover { background: ${BRAND_DARK}; }
|
||||||
|
#smcp-btn svg { width: 26px; height: 26px; }
|
||||||
|
#smcp-window {
|
||||||
|
position: fixed; bottom: 92px; right: 24px; z-index: 9999;
|
||||||
|
width: 360px; height: 520px; border-radius: 16px;
|
||||||
|
background: #fff; box-shadow: 0 8px 40px rgba(0,0,0,0.18);
|
||||||
|
display: flex; flex-direction: column; overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
#smcp-window.hidden { display: none; }
|
||||||
|
#smcp-header {
|
||||||
|
background: ${BRAND}; color: #fff;
|
||||||
|
padding: 14px 16px; display: flex; align-items: center; gap: 10px;
|
||||||
|
font-weight: 600; font-size: 15px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#smcp-header span { flex: 1; }
|
||||||
|
#smcp-close {
|
||||||
|
background: none; border: none; color: #fff; cursor: pointer;
|
||||||
|
font-size: 20px; line-height: 1; padding: 0;
|
||||||
|
}
|
||||||
|
#smcp-messages {
|
||||||
|
flex: 1; overflow-y: auto; padding: 16px;
|
||||||
|
display: flex; flex-direction: column; gap: 10px;
|
||||||
|
}
|
||||||
|
.smcp-msg {
|
||||||
|
max-width: 80%; padding: 9px 13px; border-radius: 12px;
|
||||||
|
line-height: 1.45; word-break: break-word;
|
||||||
|
}
|
||||||
|
.smcp-msg.bot {
|
||||||
|
background: #f1f0fe; color: #1a1a2e; border-bottom-left-radius: 4px; align-self: flex-start;
|
||||||
|
}
|
||||||
|
.smcp-msg.user {
|
||||||
|
background: ${BRAND}; color: #fff; border-bottom-right-radius: 4px; align-self: flex-end;
|
||||||
|
}
|
||||||
|
.smcp-msg.typing { color: #888; font-style: italic; }
|
||||||
|
#smcp-input-row {
|
||||||
|
padding: 10px 12px; border-top: 1px solid #eee;
|
||||||
|
display: flex; gap: 8px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#smcp-input {
|
||||||
|
flex: 1; border: 1px solid #ddd; border-radius: 8px;
|
||||||
|
padding: 8px 12px; font-size: 14px; outline: none;
|
||||||
|
font-family: inherit; resize: none; line-height: 1.4;
|
||||||
|
}
|
||||||
|
#smcp-input:focus { border-color: ${BRAND}; }
|
||||||
|
#smcp-send {
|
||||||
|
background: ${BRAND}; color: #fff; border: none;
|
||||||
|
border-radius: 8px; padding: 0 14px; cursor: pointer;
|
||||||
|
font-size: 18px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
#smcp-send:hover { background: ${BRAND_DARK}; }
|
||||||
|
#smcp-send:disabled { background: #ccc; cursor: default; }
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
#smcp-window { width: calc(100vw - 24px); right: 12px; bottom: 80px; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WELCOME = "Hi! I'm the SquareMCP assistant. Ask me anything about connecting your AI agent to social platforms.";
|
||||||
|
|
||||||
|
function inject() {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', `
|
||||||
|
<button id="smcp-btn" aria-label="Chat with SquareMCP">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="smcp-window" class="hidden">
|
||||||
|
<div id="smcp-header">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>SquareMCP Support</span>
|
||||||
|
<button id="smcp-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="smcp-messages"></div>
|
||||||
|
<div id="smcp-input-row">
|
||||||
|
<textarea id="smcp-input" rows="1" placeholder="Ask a question…"></textarea>
|
||||||
|
<button id="smcp-send">►</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const win = document.getElementById('smcp-window');
|
||||||
|
const btn = document.getElementById('smcp-btn');
|
||||||
|
const closeBtn = document.getElementById('smcp-close');
|
||||||
|
const messages = document.getElementById('smcp-messages');
|
||||||
|
const input = document.getElementById('smcp-input');
|
||||||
|
const send = document.getElementById('smcp-send');
|
||||||
|
|
||||||
|
const history = [];
|
||||||
|
let busy = false;
|
||||||
|
|
||||||
|
function addMsg(text, role, typing) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'smcp-msg ' + role + (typing ? ' typing' : '');
|
||||||
|
div.textContent = text;
|
||||||
|
messages.appendChild(div);
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMsg(WELCOME, 'bot');
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
win.classList.toggle('hidden');
|
||||||
|
if (!win.classList.contains('hidden')) input.focus();
|
||||||
|
});
|
||||||
|
closeBtn.addEventListener('click', () => win.classList.add('hidden'));
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
const text = input.value.trim();
|
||||||
|
if (!text || busy) return;
|
||||||
|
busy = true;
|
||||||
|
send.disabled = true;
|
||||||
|
input.value = '';
|
||||||
|
input.style.height = '';
|
||||||
|
|
||||||
|
addMsg(text, 'user');
|
||||||
|
history.push({ role: 'user', content: text });
|
||||||
|
|
||||||
|
const indicator = addMsg('Typing…', 'bot', true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(API, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ messages: history }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const reply = data.reply || 'Sorry, something went wrong. Try again.';
|
||||||
|
indicator.textContent = reply;
|
||||||
|
indicator.classList.remove('typing');
|
||||||
|
history.push({ role: 'assistant', content: reply });
|
||||||
|
} catch {
|
||||||
|
indicator.textContent = 'Network error. Please try again.';
|
||||||
|
indicator.classList.remove('typing');
|
||||||
|
}
|
||||||
|
|
||||||
|
busy = false;
|
||||||
|
send.disabled = false;
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
send.addEventListener('click', submit);
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
|
||||||
|
});
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
input.style.height = '';
|
||||||
|
input.style.height = Math.min(input.scrollHeight, 96) + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', inject);
|
||||||
|
} else {
|
||||||
|
inject();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -301,5 +301,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="./script.js?v=20260424b"></script>
|
<script src="./script.js?v=20260424b"></script>
|
||||||
</body>
|
<script src="https://hermes.squaremcp.com/chat-widget.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
49
src/chat.ts
Normal file
49
src/chat.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 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.`;
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleChat(messages: ChatMessage[]): Promise<string> {
|
||||||
|
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 block = response.content[0];
|
||||||
|
if (block.type !== 'text') throw new Error('Unexpected response type');
|
||||||
|
return block.text;
|
||||||
|
}
|
||||||
113
src/clients/slack.ts
Normal file
113
src/clients/slack.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { Customer } from '../billing/middleware.js';
|
||||||
|
import type { OAuthCredentials } from '../multitenancy/credential-store.js';
|
||||||
|
import { createToolAudit } from '../multitenancy/audit-log.js';
|
||||||
|
|
||||||
|
const SLACK_API_BASE = 'https://slack.com/api';
|
||||||
|
|
||||||
|
interface SlackCredentials extends OAuthCredentials {
|
||||||
|
channelId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnvToken(account: string): string {
|
||||||
|
const envKey = `SLACK_${account.toUpperCase()}_BOT_TOKEN`;
|
||||||
|
return process.env[envKey] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCredentials(
|
||||||
|
args: { account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{ token: string; defaultChannelId?: string }> {
|
||||||
|
if (customer) {
|
||||||
|
const creds = await customer.getCredential<SlackCredentials>('slack');
|
||||||
|
if (!creds) throw new Error('Slack not connected for this account');
|
||||||
|
return { token: creds.accessToken, defaultChannelId: creds.channelId };
|
||||||
|
}
|
||||||
|
const token = getEnvToken(args.account ?? 'default');
|
||||||
|
if (!token) throw new Error('Missing Slack credentials. Set SLACK_{ACCOUNT}_BOT_TOKEN');
|
||||||
|
return { token };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function slackRequest(token: string, method: string, body?: Record<string, unknown>) {
|
||||||
|
const res = await fetch(`${SLACK_API_BASE}/${method}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Slack HTTP error (${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json() as { ok: boolean; error?: string; [key: string]: unknown };
|
||||||
|
if (!data.ok) {
|
||||||
|
throw new Error(`Slack API error: ${data.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMe(
|
||||||
|
args: { account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{ user_id: string; user: string; team: string; team_id: string }> {
|
||||||
|
const { token } = await resolveCredentials(args, customer);
|
||||||
|
const data = await slackRequest(token, 'auth.test');
|
||||||
|
return {
|
||||||
|
user_id: data.user_id as string,
|
||||||
|
user: data.user as string,
|
||||||
|
team: data.team as string,
|
||||||
|
team_id: data.team_id as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChannels(
|
||||||
|
args: { limit?: number; account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<Array<{ id: string; name: string; is_member: boolean; num_members: number }>> {
|
||||||
|
const { token } = await resolveCredentials(args, customer);
|
||||||
|
const data = await slackRequest(token, 'conversations.list', {
|
||||||
|
limit: args.limit ?? 100,
|
||||||
|
types: 'public_channel,private_channel',
|
||||||
|
exclude_archived: true,
|
||||||
|
});
|
||||||
|
const channels = data.channels as Array<{ id: string; name: string; is_member: boolean; num_members: number }>;
|
||||||
|
return channels.map(c => ({ id: c.id, name: c.name, is_member: c.is_member, num_members: c.num_members }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessage(
|
||||||
|
args: { channel_id?: string; text: string; account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<{ ts: string; channel: string }> {
|
||||||
|
const { token, defaultChannelId } = await resolveCredentials(args, customer);
|
||||||
|
const channel = args.channel_id ?? defaultChannelId;
|
||||||
|
if (!channel) throw new Error('channel_id is required (or set a default channel when connecting)');
|
||||||
|
|
||||||
|
const audit = customer ? createToolAudit(customer.id, 'slack:sendMessage') : null;
|
||||||
|
const auditArgs = { channel };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await slackRequest(token, 'chat.postMessage', { channel, text: args.text });
|
||||||
|
if (audit) await audit.success(auditArgs);
|
||||||
|
return { ts: data.ts as string, channel: data.channel as string };
|
||||||
|
} catch (err) {
|
||||||
|
if (audit) await audit.error(auditArgs, String(err));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessages(
|
||||||
|
args: { channel_id: string; limit?: number; account?: string },
|
||||||
|
customer?: Customer
|
||||||
|
): Promise<Array<{ ts: string; text: string; user: string }>> {
|
||||||
|
const { token } = await resolveCredentials(args, customer);
|
||||||
|
const data = await slackRequest(token, 'conversations.history', {
|
||||||
|
channel: args.channel_id,
|
||||||
|
limit: args.limit ?? 10,
|
||||||
|
});
|
||||||
|
const messages = data.messages as Array<{ ts: string; text: string; user: string }>;
|
||||||
|
return messages.map(m => ({ ts: m.ts, text: m.text, user: m.user }));
|
||||||
|
}
|
||||||
18
src/index.ts
18
src/index.ts
@@ -36,6 +36,7 @@ import { getAllPlatformHealth } from './multitenancy/platform-health.js';
|
|||||||
import { deliverWebhook, isValidWebhookUrl } from './webhooks/delivery.js';
|
import { deliverWebhook, isValidWebhookUrl } from './webhooks/delivery.js';
|
||||||
import { notifyNewPilotRequest } from './notifications/index.js';
|
import { notifyNewPilotRequest } from './notifications/index.js';
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
|
import { handleChat, type ChatMessage } from './chat.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
@@ -57,6 +58,7 @@ app.use(express.urlencoded({ extended: true }));
|
|||||||
|
|
||||||
// ── Static files (videos, assets) ──────────────────────────────────────────
|
// ── Static files (videos, assets) ──────────────────────────────────────────
|
||||||
app.use('/public', express.static('/vaults/public'));
|
app.use('/public', express.static('/vaults/public'));
|
||||||
|
app.use(express.static(new URL('../../product', import.meta.url).pathname));
|
||||||
|
|
||||||
// ── Config ─────────────────────────────────────────────────────────────────
|
// ── Config ─────────────────────────────────────────────────────────────────
|
||||||
const PORT = process.env.PORT ?? 3456;
|
const PORT = process.env.PORT ?? 3456;
|
||||||
@@ -1978,6 +1980,22 @@ app.post('/api/facebook/video', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Chat widget endpoint ────────────────────────────────────────
|
||||||
|
app.post('/api/chat', async (req, res) => {
|
||||||
|
const { messages } = req.body as { messages?: ChatMessage[] };
|
||||||
|
if (!Array.isArray(messages) || messages.length === 0) {
|
||||||
|
res.status(400).json({ error: 'messages array required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const reply = await handleChat(messages);
|
||||||
|
res.json({ reply });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[chat] error:', (err as Error).message);
|
||||||
|
res.status(500).json({ error: 'Chat unavailable' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── TikTok REST endpoints ───────────────────────────────────────
|
// ── TikTok REST endpoints ───────────────────────────────────────
|
||||||
app.get('/api/tiktok/profile', requireAuth, async (req, res) => {
|
app.get('/api/tiktok/profile', requireAuth, async (req, res) => {
|
||||||
const account = req.query.account as string | undefined;
|
const account = req.query.account as string | undefined;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function decrypt(ciphertext: string): string {
|
|||||||
return decipher.update(encrypted) + decipher.final('utf8');
|
return decipher.update(encrypted) + decipher.final('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Platform = 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'discord' | 'instagram' | 'twitter' | 'tiktok' | 'snapchat' | 'facebook' | 'obsidian';
|
export type Platform = 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'discord' | 'instagram' | 'twitter' | 'tiktok' | 'snapchat' | 'facebook' | 'obsidian' | 'slack';
|
||||||
|
|
||||||
export interface EmailCredentials {
|
export interface EmailCredentials {
|
||||||
host: string;
|
host: string;
|
||||||
|
|||||||
86
src/tools.ts
86
src/tools.ts
@@ -14,6 +14,7 @@ import { searchTweets, getUserProfile, getUserTweets, createTweet, uploadVideoAn
|
|||||||
import { getUserProfile as getTikTokProfile, getCreatorInfo, createVideo, getVideoStatus } from './clients/tiktok.js';
|
import { getUserProfile as getTikTokProfile, getCreatorInfo, createVideo, getVideoStatus } from './clients/tiktok.js';
|
||||||
import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
|
import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
|
||||||
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js';
|
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js';
|
||||||
|
import { getMe as getSlackMe, getChannels as getSlackChannels, sendMessage as sendSlackMessage, getMessages as getSlackMessages } from './clients/slack.js';
|
||||||
|
|
||||||
const ACCOUNT_PARAM = {
|
const ACCOUNT_PARAM = {
|
||||||
account: {
|
account: {
|
||||||
@@ -443,6 +444,59 @@ export const tools: Tool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Slack tools ──────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'slack_get_me',
|
||||||
|
description:
|
||||||
|
'Verify the Slack bot is connected and return workspace info. Use to confirm credentials are working.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slack_get_channels',
|
||||||
|
description:
|
||||||
|
'List Slack channels the bot has access to. Use to find channel IDs before sending messages.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'number', description: 'Max channels to return (default: 100)' },
|
||||||
|
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slack_send_message',
|
||||||
|
description:
|
||||||
|
'Send a message to a Slack channel. Uses the default channel if channel_id is omitted.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['text'],
|
||||||
|
properties: {
|
||||||
|
channel_id: { type: 'string', description: 'Slack channel ID (e.g. C0123456). Uses default channel if omitted.' },
|
||||||
|
text: { type: 'string', description: 'Message text. Supports Slack mrkdwn formatting.' },
|
||||||
|
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slack_get_messages',
|
||||||
|
description:
|
||||||
|
'Get recent messages from a Slack channel.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['channel_id'],
|
||||||
|
properties: {
|
||||||
|
channel_id: { type: 'string', description: 'Slack channel ID' },
|
||||||
|
limit: { type: 'number', description: 'Max messages to return (default: 10)' },
|
||||||
|
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// ── Instagram tools ──────────────────────────────────────────
|
// ── Instagram tools ──────────────────────────────────────────
|
||||||
{
|
{
|
||||||
name: 'instagram_get_profile',
|
name: 'instagram_get_profile',
|
||||||
@@ -738,7 +792,7 @@ async function resolveEmailCtx(args: Record<string, unknown>, customer?: Custome
|
|||||||
|
|
||||||
const PLATFORM_PREFIXES = [
|
const PLATFORM_PREFIXES = [
|
||||||
'linkedin', 'obsidian', 'whatsapp', 'telegram', 'discord',
|
'linkedin', 'obsidian', 'whatsapp', 'telegram', 'discord',
|
||||||
'instagram', 'twitter', 'tiktok', 'snapchat', 'facebook',
|
'instagram', 'twitter', 'tiktok', 'snapchat', 'facebook', 'slack',
|
||||||
];
|
];
|
||||||
|
|
||||||
function toolPlatform(name: string): string {
|
function toolPlatform(name: string): string {
|
||||||
@@ -984,6 +1038,36 @@ export async function handleToolCall(
|
|||||||
}, customer);
|
}, customer);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ── Slack ───────────────────────────────────────────────────
|
||||||
|
case 'slack_get_me':
|
||||||
|
result = await getSlackMe({
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'slack_get_channels':
|
||||||
|
result = await getSlackChannels({
|
||||||
|
limit: args.limit as number | undefined,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'slack_send_message':
|
||||||
|
result = await sendSlackMessage({
|
||||||
|
channel_id: args.channel_id as string | undefined,
|
||||||
|
text: args.text as string,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'slack_get_messages':
|
||||||
|
result = await getSlackMessages({
|
||||||
|
channel_id: args.channel_id as string,
|
||||||
|
limit: args.limit as number | undefined,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
}, customer);
|
||||||
|
break;
|
||||||
|
|
||||||
// ── Instagram ───────────────────────────────────────────────
|
// ── Instagram ───────────────────────────────────────────────
|
||||||
case 'instagram_get_profile':
|
case 'instagram_get_profile':
|
||||||
result = await getInstagramProfile({
|
result = await getInstagramProfile({
|
||||||
|
|||||||
Reference in New Issue
Block a user