feat: WhatsApp + LinkedIn integrations, SquareMCP rebrand, opencode docs
WhatsApp Business API (Meta Cloud API)
- New client: src/clients/whatsapp.ts
- Tools: whatsapp_send_message, whatsapp_send_template, whatsapp_list_templates
- REST endpoints: POST /api/whatsapp/send, POST /api/whatsapp/template, GET /api/whatsapp/templates
- Multi-account env var pattern: WHATSAPP_{ACCOUNT}_*
LinkedIn API (OpenID Connect)
- New client: src/clients/linkedin.ts
- Tools: linkedin_get_profile, linkedin_create_post, linkedin_search_connections, linkedin_send_message
- REST endpoints: GET /api/linkedin/profile, POST /api/linkedin/post, POST /api/linkedin/search-connections, POST /api/linkedin/message
- Multi-account env var pattern: LINKEDIN_{ACCOUNT}_*
- Uses /v2/userinfo (OpenID Connect) for profile reads
Domain migration
- hermes.fetcherpay.com -> hermes.squaremcp.com
- Updated K8s ingress, TLS cert, SERVER_URL env var
- Updated OPENCODE.md and opencode.json references
SquareMCP site
- Added logo assets (SVG, LinkedIn variants)
- Added terms.html
- Updated Dockerfile, nginx config, styles, index, privacy pages
Docs
- Added OPENCODE.md for opencode AI integration setup
- Updated .env.example with WhatsApp and LinkedIn credentials
- Added opencode.json to .gitignore (contains live API key)
Total tools: 19 (email 6, obsidian 5, whatsapp 4, linkedin 4)
This commit is contained in:
17
.env.example
17
.env.example
@@ -37,3 +37,20 @@ SYNCTHING_URL=http://host.docker.internal:8384
|
|||||||
SYNCTHING_API_KEY=your-syncthing-api-key
|
SYNCTHING_API_KEY=your-syncthing-api-key
|
||||||
# Folder ID as set in Syncthing config
|
# Folder ID as set in Syncthing config
|
||||||
SYNCTHING_FOLDER_ID=obsidian-vault
|
SYNCTHING_FOLDER_ID=obsidian-vault
|
||||||
|
|
||||||
|
# ── WhatsApp Business API (Meta Cloud API) ───────────────────────────────────
|
||||||
|
# Get these from https://business.facebook.com/settings/whatsapp-business-accounts
|
||||||
|
# For default account:
|
||||||
|
WHATSAPP_DEFAULT_PHONE_NUMBER_ID=your-phone-number-id
|
||||||
|
WHATSAPP_DEFAULT_ACCESS_TOKEN=your-permanent-access-token
|
||||||
|
WHATSAPP_DEFAULT_BUSINESS_ACCOUNT_ID=your-business-account-id
|
||||||
|
# For additional accounts, duplicate with WHATSAPP_{ACCOUNT}_*
|
||||||
|
|
||||||
|
# ── LinkedIn ─────────────────────────────────────────────────────────────────
|
||||||
|
# Get an access token from LinkedIn Developer Portal: https://www.linkedin.com/developers/
|
||||||
|
# Required scopes: r_liteprofile, w_member_social
|
||||||
|
# For default account:
|
||||||
|
LINKEDIN_DEFAULT_ACCESS_TOKEN=your-linkedin-access-token
|
||||||
|
LINKEDIN_DEFAULT_CLIENT_ID=your-linkedin-client-id
|
||||||
|
LINKEDIN_DEFAULT_CLIENT_SECRET=your-linkedin-client-secret
|
||||||
|
# For additional accounts, duplicate with LINKEDIN_{ACCOUNT}_*
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ README.private.md
|
|||||||
hermes.log
|
hermes.log
|
||||||
.claude/
|
.claude/
|
||||||
.codex
|
.codex
|
||||||
|
opencode.json
|
||||||
|
|||||||
93
OPENCODE.md
Normal file
93
OPENCODE.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Connecting hermes-mcp to opencode AI
|
||||||
|
|
||||||
|
The server is deployed at `https://hermes.squaremcp.com` with a Streamable HTTP MCP endpoint (MCP 1.x).
|
||||||
|
|
||||||
|
## Quick setup
|
||||||
|
|
||||||
|
An `opencode.json` is already in this repo with the connection pre-configured. Open opencode in this directory and hermes-mcp will be available automatically.
|
||||||
|
|
||||||
|
## Manual configuration
|
||||||
|
|
||||||
|
### Project-level (`opencode.json` in project root)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"hermes": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://hermes.squaremcp.com/mcp",
|
||||||
|
"headers": {
|
||||||
|
"x-api-key": "YOUR_MCP_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global (`~/.config/opencode/config.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"hermes": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://hermes.squaremcp.com/mcp",
|
||||||
|
"headers": {
|
||||||
|
"x-api-key": "YOUR_MCP_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `YOUR_MCP_API_KEY` with the value of `MCP_API_KEY` from your `.env` or Kubernetes secret.
|
||||||
|
|
||||||
|
## Connection details
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Transport | Streamable HTTP (MCP 1.x) |
|
||||||
|
| Endpoint | `https://hermes.squaremcp.com/mcp` |
|
||||||
|
| Auth | `x-api-key` header (or `?key=` query param, or `Authorization: Bearer`) |
|
||||||
|
| Legacy SSE | `https://hermes.squaremcp.com/sse` |
|
||||||
|
|
||||||
|
## Fallback: SSE transport
|
||||||
|
|
||||||
|
If opencode requires SSE transport instead of Streamable HTTP:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"hermes": {
|
||||||
|
"type": "sse",
|
||||||
|
"url": "https://hermes.squaremcp.com/sse",
|
||||||
|
"headers": {
|
||||||
|
"x-api-key": "YOUR_MCP_API_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `get_profile` | Get email address for an account |
|
||||||
|
| `search_messages` | Search INBOX by keyword / sender / subject |
|
||||||
|
| `read_message` | Read full message body by UID |
|
||||||
|
| `list_folders` | List all mailbox folders |
|
||||||
|
| `create_draft` | Save a draft to the Drafts folder |
|
||||||
|
| `send_email` | Send an email |
|
||||||
|
| `whatsapp_send_message` | Send a WhatsApp message |
|
||||||
|
| `whatsapp_send_template` | Send an approved WhatsApp template message |
|
||||||
|
| `whatsapp_get_message_status` | Check WhatsApp message delivery status |
|
||||||
|
| `whatsapp_list_templates` | List approved WhatsApp templates |
|
||||||
|
| `obsidian_search_notes` | Search notes in the Obsidian vault |
|
||||||
|
| `obsidian_read_note` | Read a specific note |
|
||||||
|
| `obsidian_append_to_note` | Append content to a note |
|
||||||
|
| `obsidian_update_note` | Overwrite a note |
|
||||||
|
| `obsidian_sync_status` | Check Obsidian sync status |
|
||||||
@@ -4,6 +4,8 @@ COPY product/site/nginx-site.conf /etc/nginx/conf.d/default.conf
|
|||||||
COPY product/site/index.html /usr/share/nginx/html/index.html
|
COPY product/site/index.html /usr/share/nginx/html/index.html
|
||||||
COPY product/site/styles.css /usr/share/nginx/html/styles.css
|
COPY product/site/styles.css /usr/share/nginx/html/styles.css
|
||||||
COPY product/site/script.js /usr/share/nginx/html/script.js
|
COPY product/site/script.js /usr/share/nginx/html/script.js
|
||||||
|
COPY product/site/squaremcp-logo.svg /usr/share/nginx/html/squaremcp-logo.svg
|
||||||
COPY product/site/squaremcp_launch.gif /usr/share/nginx/html/squaremcp_launch.gif
|
COPY product/site/squaremcp_launch.gif /usr/share/nginx/html/squaremcp_launch.gif
|
||||||
COPY product/site/squaremcp_launch_poster.png /usr/share/nginx/html/squaremcp_launch_poster.png
|
COPY product/site/squaremcp_launch_poster.png /usr/share/nginx/html/squaremcp_launch_poster.png
|
||||||
COPY product/site/privacy.html /usr/share/nginx/html/privacy.html
|
COPY product/site/privacy.html /usr/share/nginx/html/privacy.html
|
||||||
|
COPY product/site/terms.html /usr/share/nginx/html/terms.html
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="SquareMCP is a managed MCP gateway for internal tools with authentication, permissions, audit logs, and observability."
|
content="SquareMCP is a managed MCP gateway for internal tools with authentication, permissions, audit logs, and observability."
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="./styles.css?v=20260424b" />
|
<link rel="stylesheet" href="./styles.css?v=20260505b" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="topbar">
|
<nav class="topbar">
|
||||||
<div class="wrap topbar-row">
|
<div class="wrap topbar-row">
|
||||||
<a class="brand" href="/">
|
<a class="brand" href="/">
|
||||||
<span class="brand-mark">S</span>
|
<img class="brand-logo" src="./squaremcp-logo.svg" alt="" />
|
||||||
<span class="brand-text">SquareMCP</span>
|
<span class="brand-text">SquareMCP</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
@@ -294,6 +294,8 @@
|
|||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a class="footer-link" href="mailto:info@squaremcp.com">info@squaremcp.com</a>
|
<a class="footer-link" href="mailto:info@squaremcp.com">info@squaremcp.com</a>
|
||||||
<a class="footer-link" href="https://squaremcp.com">squaremcp.com</a>
|
<a class="footer-link" href="https://squaremcp.com">squaremcp.com</a>
|
||||||
|
<a class="footer-link" href="/privacy">Privacy</a>
|
||||||
|
<a class="footer-link" href="/terms">Terms</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ server {
|
|||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri.html $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /index.html {
|
location = /index.html {
|
||||||
|
|||||||
@@ -1,41 +1,136 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Privacy Policy — SquareMCP</title>
|
<title>Privacy Policy — SquareMCP</title>
|
||||||
<style>
|
<meta
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 680px; margin: 60px auto; padding: 0 24px; color: #111; line-height: 1.7; }
|
name="description"
|
||||||
h1 { font-size: 1.6rem; margin-bottom: 0.25rem; }
|
content="SquareMCP privacy policy covering pilot requests, service data, and how customer environments are handled."
|
||||||
.sub { color: #666; font-size: 0.9rem; margin-bottom: 2rem; }
|
/>
|
||||||
h2 { font-size: 1.05rem; margin-top: 2rem; }
|
<link rel="stylesheet" href="./styles.css?v=20260505b" />
|
||||||
a { color: #111; }
|
</head>
|
||||||
nav { margin-bottom: 2rem; font-size: 0.9rem; }
|
<body class="legal-shell">
|
||||||
nav a { text-decoration: none; color: #666; }
|
<nav class="topbar">
|
||||||
nav a:hover { color: #111; }
|
<div class="wrap topbar-row">
|
||||||
</style>
|
<a class="brand" href="/">
|
||||||
</head>
|
<img class="brand-logo" src="./squaremcp-logo.svg" alt="" />
|
||||||
<body>
|
<span class="brand-text">SquareMCP</span>
|
||||||
<nav><a href="/">← squaremcp.com</a></nav>
|
</a>
|
||||||
<h1>Privacy Policy</h1>
|
<div class="topbar-actions">
|
||||||
<p class="sub">SquareMCP — Last updated April 28, 2026</p>
|
<a class="topbar-link" href="/terms">Terms</a>
|
||||||
|
<a class="button secondary" href="mailto:info@squaremcp.com">Contact</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<h2>What SquareMCP is</h2>
|
<main class="legal-main">
|
||||||
<p>SquareMCP is a personal MCP server platform that connects AI assistants to your own tools — email, notes, and internal systems. It is operated by Garfield Heron and is currently in private pilot.</p>
|
<article class="legal-card">
|
||||||
|
<div class="legal-eyebrow">Legal</div>
|
||||||
|
<h1 class="legal-title">Privacy Policy</h1>
|
||||||
|
<p class="legal-subhead">Last updated May 5, 2026</p>
|
||||||
|
|
||||||
<h2>Data we collect</h2>
|
<section class="legal-section">
|
||||||
<p>We collect only what is necessary to operate the service: your name, email address, company, and use case when you submit a pilot request. This information is stored securely and used only to evaluate and onboard pilot participants.</p>
|
<h2>Scope</h2>
|
||||||
|
<p>
|
||||||
|
This Privacy Policy describes how SquareMCP collects, uses, and protects information
|
||||||
|
when you visit squaremcp.com, contact us, or participate in a SquareMCP pilot or
|
||||||
|
managed deployment.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2>Data we do not collect</h2>
|
<section class="legal-section">
|
||||||
<p>SquareMCP does not use analytics, advertising trackers, or third-party data sharing of any kind. We do not sell or rent your information.</p>
|
<h2>Information we collect</h2>
|
||||||
|
<p>We may collect:</p>
|
||||||
|
<ul>
|
||||||
|
<li>contact details such as your name, work email, company, and role</li>
|
||||||
|
<li>pilot intake details such as your use case, target systems, and security requirements</li>
|
||||||
|
<li>service and operational data needed to provision, secure, and support a deployment</li>
|
||||||
|
<li>communications you send to us by email or through the pilot intake form</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2>Your data and your tools</h2>
|
<section class="legal-section">
|
||||||
<p>When you use SquareMCP to connect your own accounts (email, notes, calendars), those connections are authenticated via OAuth and run entirely within your own environment. Your data does not pass through SquareMCP infrastructure — the MCP server runs on your own host or a host you control.</p>
|
<h2>How we use information</h2>
|
||||||
|
<p>We use information to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>review and respond to pilot requests</li>
|
||||||
|
<li>configure and operate SquareMCP deployments</li>
|
||||||
|
<li>authenticate access, troubleshoot issues, and maintain security controls</li>
|
||||||
|
<li>communicate about pilots, support, billing, and service changes</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2>OAuth tokens</h2>
|
<section class="legal-section">
|
||||||
<p>OAuth access tokens used to authenticate AI sessions are stored in a private database on your server. They expire after 24 hours and are never shared with third parties.</p>
|
<h2>Customer data and connected systems</h2>
|
||||||
|
<p>
|
||||||
|
SquareMCP is designed to act as a managed MCP gateway for internal tools. Depending on
|
||||||
|
the deployment, customer data may remain in a customer-controlled environment or may be
|
||||||
|
processed in SquareMCP-managed infrastructure as part of the service. The exact data
|
||||||
|
path depends on the deployment architecture and connector configuration.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Pilot and production customers are responsible for evaluating which systems they choose
|
||||||
|
to connect and which tool permissions they enable for their users and agents.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h2>Contact</h2>
|
<section class="legal-section">
|
||||||
<p>Questions or requests: <a href="mailto:garfield@fetcherpay.com">garfield@fetcherpay.com</a></p>
|
<h2>Authentication credentials and tokens</h2>
|
||||||
</body>
|
<p>
|
||||||
|
SquareMCP may process API keys, OAuth credentials, session metadata, audit records, and
|
||||||
|
related access-control data needed to operate the service. We use these credentials only
|
||||||
|
to authenticate approved integrations and support the configured deployment.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Sharing</h2>
|
||||||
|
<p>
|
||||||
|
We do not sell personal information. We may share information with infrastructure,
|
||||||
|
hosting, email, or support providers only to the extent reasonably necessary to run the
|
||||||
|
service, support customers, comply with law, or protect SquareMCP and its users.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Retention</h2>
|
||||||
|
<p>
|
||||||
|
We retain information for as long as reasonably necessary to evaluate pilots, deliver
|
||||||
|
services, maintain records, and meet legal, operational, or security obligations.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Security</h2>
|
||||||
|
<p>
|
||||||
|
We use reasonable administrative, technical, and operational measures to protect
|
||||||
|
information. No system can guarantee absolute security, and you should not submit
|
||||||
|
information through the service unless you are comfortable with that risk profile.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Your choices</h2>
|
||||||
|
<p>
|
||||||
|
You may contact us to request access, correction, or deletion of personal information we
|
||||||
|
hold about you, subject to legal and operational limits.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
Questions about this Privacy Policy can be sent to
|
||||||
|
<a class="footer-link" href="mailto:info@squaremcp.com">info@squaremcp.com</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="legal-note">
|
||||||
|
This page is a general website and pilot-stage privacy policy. It should be reviewed and
|
||||||
|
adapted if SquareMCP moves into broader commercial availability or regulated deployments.
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
BIN
product/site/squaremcp-logo-linkedin-transparent.png
Normal file
BIN
product/site/squaremcp-logo-linkedin-transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
product/site/squaremcp-logo-linkedin.jpg
Normal file
BIN
product/site/squaremcp-logo-linkedin.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
product/site/squaremcp-logo-linkedin.png
Normal file
BIN
product/site/squaremcp-logo-linkedin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
18
product/site/squaremcp-logo.svg
Normal file
18
product/site/squaremcp-logo.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>SquareMCP logo</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="squaremcp-logo-gradient" x1="10" y1="10" x2="54" y2="54" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#7DB6FF"/>
|
||||||
|
<stop offset="1" stop-color="#0E63F6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
d="M10 12C10 10.8954 10.8954 10 12 10H31V17H17V31H10V12ZM33 10H52C53.1046 10 54 10.8954 54 12V31H47V17H33V10ZM10 33H17V47H31V54H12C10.8954 54 10 53.1046 10 52V33ZM47 33H54V52C54 53.1046 53.1046 54 52 54H33V47H47V33Z"
|
||||||
|
fill="url(#squaremcp-logo-gradient)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M24 24H33V31H40V40H31V33H24V24Z"
|
||||||
|
fill="#0E63F6"
|
||||||
|
opacity="0.92"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 736 B |
@@ -58,15 +58,11 @@ a {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.brand-logo {
|
||||||
display: inline-flex;
|
display: block;
|
||||||
align-items: center;
|
width: 34px;
|
||||||
justify-content: center;
|
height: 34px;
|
||||||
width: 30px;
|
flex: 0 0 auto;
|
||||||
height: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-text {
|
.brand-text {
|
||||||
@@ -465,6 +461,72 @@ textarea:focus {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legal-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, #d9e6ff 0%, transparent 24%),
|
||||||
|
linear-gradient(180deg, #f8fbff 0%, #eef3fb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-main {
|
||||||
|
padding: 40px 0 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-card {
|
||||||
|
width: min(860px, calc(100% - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 18px 40px rgba(22, 41, 76, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-eyebrow {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-subhead {
|
||||||
|
margin: 0 0 28px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-section + .legal-section {
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-section h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-section p,
|
||||||
|
.legal-section li {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-section ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-note {
|
||||||
|
margin-top: 28px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
@@ -517,4 +579,16 @@ textarea:focus {
|
|||||||
.band {
|
.band {
|
||||||
padding: 44px 0;
|
padding: 44px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legal-main {
|
||||||
|
padding: 28px 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-card {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-section h2 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
140
product/site/terms.html
Normal file
140
product/site/terms.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Terms of Service — SquareMCP</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="SquareMCP terms covering pilot access, acceptable use, customer responsibilities, and service limitations."
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="./styles.css?v=20260505b" />
|
||||||
|
</head>
|
||||||
|
<body class="legal-shell">
|
||||||
|
<nav class="topbar">
|
||||||
|
<div class="wrap topbar-row">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img class="brand-logo" src="./squaremcp-logo.svg" alt="" />
|
||||||
|
<span class="brand-text">SquareMCP</span>
|
||||||
|
</a>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<a class="topbar-link" href="/privacy">Privacy</a>
|
||||||
|
<a class="button secondary" href="mailto:info@squaremcp.com">Contact</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="legal-main">
|
||||||
|
<article class="legal-card">
|
||||||
|
<div class="legal-eyebrow">Legal</div>
|
||||||
|
<h1 class="legal-title">Terms of Service</h1>
|
||||||
|
<p class="legal-subhead">Last updated May 5, 2026</p>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Agreement</h2>
|
||||||
|
<p>
|
||||||
|
These Terms of Service govern your access to and use of SquareMCP, including the
|
||||||
|
squaremcp.com website, pilot engagements, managed deployments, and related support.
|
||||||
|
By using SquareMCP, you agree to these Terms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Service description</h2>
|
||||||
|
<p>
|
||||||
|
SquareMCP provides managed MCP infrastructure and related services for connecting AI
|
||||||
|
agents to customer-approved internal tools, systems, and data sources.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Pilot and beta status</h2>
|
||||||
|
<p>
|
||||||
|
Some SquareMCP offerings are pilot, beta, or limited-availability services. Features
|
||||||
|
may change, and services may be modified, suspended, or discontinued at any time,
|
||||||
|
especially during pilot phases.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Customer responsibilities</h2>
|
||||||
|
<p>You are responsible for:</p>
|
||||||
|
<ul>
|
||||||
|
<li>providing accurate information during pilot intake and onboarding</li>
|
||||||
|
<li>ensuring you have authority to connect systems, accounts, and data sources</li>
|
||||||
|
<li>configuring appropriate permissions, approvals, and internal safeguards</li>
|
||||||
|
<li>reviewing agent behavior and tool outputs before relying on them in production workflows</li>
|
||||||
|
<li>complying with applicable laws, regulations, and contractual obligations</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Acceptable use</h2>
|
||||||
|
<p>You may not use SquareMCP to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>access systems or data without authorization</li>
|
||||||
|
<li>bypass security controls or usage restrictions</li>
|
||||||
|
<li>interfere with the service or other users</li>
|
||||||
|
<li>process unlawful, infringing, or abusive content or workflows</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Third-party systems</h2>
|
||||||
|
<p>
|
||||||
|
SquareMCP depends on third-party providers, APIs, models, cloud services, and customer
|
||||||
|
systems. We are not responsible for downtime, changes, pricing, restrictions, or data
|
||||||
|
handling practices of those third parties.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>No warranty</h2>
|
||||||
|
<p>
|
||||||
|
SquareMCP is provided on an “as is” and “as available” basis. To the maximum extent
|
||||||
|
permitted by law, SquareMCP disclaims all warranties, express or implied, including
|
||||||
|
warranties of merchantability, fitness for a particular purpose, and non-infringement.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Limitation of liability</h2>
|
||||||
|
<p>
|
||||||
|
To the maximum extent permitted by law, SquareMCP will not be liable for indirect,
|
||||||
|
incidental, special, consequential, exemplary, or punitive damages, or for loss of
|
||||||
|
data, revenue, profits, or goodwill arising from or related to the service.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Suspension and termination</h2>
|
||||||
|
<p>
|
||||||
|
We may suspend or terminate access if necessary to protect the service, comply with law,
|
||||||
|
address security risks, or respond to a breach of these Terms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Changes</h2>
|
||||||
|
<p>
|
||||||
|
We may update these Terms from time to time. Continued use of SquareMCP after updated
|
||||||
|
Terms are posted constitutes acceptance of the revised Terms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="legal-section">
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
Questions about these Terms can be sent to
|
||||||
|
<a class="footer-link" href="mailto:info@squaremcp.com">info@squaremcp.com</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="legal-note">
|
||||||
|
These Terms are a practical baseline for the current SquareMCP pilot site. They should be
|
||||||
|
reviewed by counsel before broad commercial rollout or regulated-enterprise contracting.
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
142
src/clients/linkedin.ts
Normal file
142
src/clients/linkedin.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
const LINKEDIN_API_BASE = 'https://api.linkedin.com/v2';
|
||||||
|
|
||||||
|
function getEnvVar(account: string, key: string): string {
|
||||||
|
const envKey = `LINKEDIN_${account.toUpperCase()}_${key}`;
|
||||||
|
return process.env[envKey] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccessToken(account: string): string {
|
||||||
|
return getEnvVar(account, 'ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkedinRequest(
|
||||||
|
endpoint: string,
|
||||||
|
accessToken: string,
|
||||||
|
method: 'GET' | 'POST' = 'GET',
|
||||||
|
body?: unknown
|
||||||
|
) {
|
||||||
|
const url = `${LINKEDIN_API_BASE}${endpoint}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Restli-Protocol-Version': '2.0.0',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text();
|
||||||
|
throw new Error(`LinkedIn API error (${res.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProfile(args: { account?: string }): Promise<{
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
picture?: string;
|
||||||
|
}> {
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenID Connect userinfo endpoint (works with profile scope)
|
||||||
|
const res = await fetch(`${LINKEDIN_API_BASE}/userinfo`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text();
|
||||||
|
throw new Error(`LinkedIn API error (${res.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
id: data.sub,
|
||||||
|
firstName: data.given_name ?? '',
|
||||||
|
lastName: data.family_name ?? '',
|
||||||
|
email: data.email ?? '',
|
||||||
|
picture: data.picture ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPost(args: {
|
||||||
|
text: string;
|
||||||
|
visibility?: 'PUBLIC' | 'CONNECTIONS';
|
||||||
|
account?: string;
|
||||||
|
}): Promise<{ success: boolean; post_id: string; url: string }> {
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await getProfile({ account: args.account });
|
||||||
|
const authorUrn = `urn:li:person:${profile.id}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
author: authorUrn,
|
||||||
|
lifecycleState: 'PUBLISHED',
|
||||||
|
specificContent: {
|
||||||
|
'com.linkedin.ugc.ShareContent': {
|
||||||
|
shareCommentary: {
|
||||||
|
text: args.text,
|
||||||
|
},
|
||||||
|
shareMediaCategory: 'NONE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibility: {
|
||||||
|
'com.linkedin.ugc.MemberNetworkVisibility': args.visibility ?? 'PUBLIC',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await linkedinRequest('/ugcPosts', accessToken, 'POST', body);
|
||||||
|
const postId = data.id ?? '';
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
post_id: postId,
|
||||||
|
url: postId ? `https://www.linkedin.com/feed/update/${postId}` : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchConnections(args: {
|
||||||
|
keywords?: string;
|
||||||
|
account?: string;
|
||||||
|
}): Promise<{ message: string }> {
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'LinkedIn connections search requires the LinkedIn Partnership Program. ' +
|
||||||
|
'Public API access to connections was removed. ' +
|
||||||
|
'Apply at https://developer.linkedin.com/partner-programs'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessage(args: {
|
||||||
|
recipient_id: string;
|
||||||
|
message: string;
|
||||||
|
account?: string;
|
||||||
|
}): Promise<{ message: string }> {
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'LinkedIn messaging requires the LinkedIn Partnership Program. ' +
|
||||||
|
'Direct messaging is not available through the public API. ' +
|
||||||
|
'Apply at https://developer.linkedin.com/partner-programs'
|
||||||
|
);
|
||||||
|
}
|
||||||
145
src/clients/whatsapp.ts
Normal file
145
src/clients/whatsapp.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const WHATSAPP_API_VERSION = 'v18.0';
|
||||||
|
const WHATSAPP_BASE_URL = process.env['WHATSAPP_API_BASE_URL'] ?? 'https://graph.facebook.com';
|
||||||
|
|
||||||
|
function getEnvVar(prefix: string, account: string, key: string): string {
|
||||||
|
const envKey = `WHATSAPP_${account.toUpperCase()}_${key}`;
|
||||||
|
return process.env[envKey] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPhoneNumberId(account: string): string {
|
||||||
|
return getEnvVar('WHATSAPP', account, 'PHONE_NUMBER_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccessToken(account: string): string {
|
||||||
|
return getEnvVar('WHATSAPP', account, 'ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBusinessAccountId(account: string): string {
|
||||||
|
return getEnvVar('WHATSAPP', account, 'BUSINESS_ACCOUNT_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WhatsAppMessageResponse {
|
||||||
|
messaging_product: string;
|
||||||
|
contacts?: Array<{ wa_id: string; input: string }>;
|
||||||
|
messages?: Array<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function whatsappApiRequest(
|
||||||
|
phoneId: string,
|
||||||
|
accessToken: string,
|
||||||
|
endpoint: string,
|
||||||
|
method: 'GET' | 'POST' = 'POST',
|
||||||
|
body?: unknown
|
||||||
|
) {
|
||||||
|
const url = `${WHATSAPP_BASE_URL}/${WHATSAPP_API_VERSION}/${phoneId}/${endpoint}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text();
|
||||||
|
throw new Error(`WhatsApp API error (${res.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessage(args: { to: string; message: string; account?: string }): Promise<{ success: boolean; message_id: string }> {
|
||||||
|
const phoneId = getPhoneNumberId(args.account ?? 'default');
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
|
||||||
|
if (!phoneId || !accessToken) {
|
||||||
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
messaging_product: 'whatsapp',
|
||||||
|
to: args.to,
|
||||||
|
type: 'text',
|
||||||
|
text: { body: args.message },
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await whatsappApiRequest(phoneId, accessToken, 'messages', 'POST', body);
|
||||||
|
const response = data as WhatsAppMessageResponse;
|
||||||
|
return { success: true, message_id: response.messages?.[0]?.id ?? '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendTemplate(args: { to: string; template_name: string; language?: string; components?: unknown[]; account?: string }): Promise<{ success: boolean; message_id: string }> {
|
||||||
|
const phoneId = getPhoneNumberId(args.account ?? 'default');
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
|
||||||
|
if (!phoneId || !accessToken) {
|
||||||
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
messaging_product: 'whatsapp',
|
||||||
|
to: args.to,
|
||||||
|
type: 'template',
|
||||||
|
template: {
|
||||||
|
name: args.template_name,
|
||||||
|
language: { code: args.language ?? 'en' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.components) {
|
||||||
|
(body.template as Record<string, unknown>).components = args.components;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await whatsappApiRequest(phoneId, accessToken, 'messages', 'POST', body);
|
||||||
|
const response = data as WhatsAppMessageResponse;
|
||||||
|
return { success: true, message_id: response.messages?.[0]?.id ?? '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessageStatus(args: { message_id: string; account?: string }): Promise<{ message_id: string; status: string; timestamp?: string }> {
|
||||||
|
const phoneId = getPhoneNumberId(args.account ?? 'default');
|
||||||
|
const accessToken = getAccessToken(args.account ?? 'default');
|
||||||
|
|
||||||
|
if (!phoneId || !accessToken) {
|
||||||
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Meta Cloud API doesn't support polling message status via GET
|
||||||
|
// Status updates are only available via webhooks (push-based)
|
||||||
|
throw new Error('whatsapp_get_message_status is not supported. Meta Cloud API only provides delivery status via webhooks. Use POST /api/whatsapp/webhook to receive status updates.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTemplates(args: { account?: string }): Promise<{ templates: Array<{ name: string; language: string; status: string }> }> {
|
||||||
|
const account = args.account ?? 'default';
|
||||||
|
const businessAccountId = getBusinessAccountId(account);
|
||||||
|
const accessToken = getAccessToken(account);
|
||||||
|
|
||||||
|
if (!businessAccountId || !accessToken) {
|
||||||
|
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_BUSINESS_ACCOUNT_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${WHATSAPP_BASE_URL}/${WHATSAPP_API_VERSION}/${businessAccountId}/message_templates?fields=name,language,status`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text();
|
||||||
|
throw new Error(`WhatsApp API error (${res.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
templates: (data.data ?? []).map((t: { name: string; language: string | { code: string }; status: string }) => ({
|
||||||
|
name: t.name,
|
||||||
|
language: typeof t.language === 'string' ? t.language : t.language?.code ?? 'en',
|
||||||
|
status: t.status,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
76
src/index.ts
76
src/index.ts
@@ -547,6 +547,82 @@ app.get('/api/obsidian/sync', requireAuth, async (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── WhatsApp Business API REST endpoints ──────────────────────────
|
||||||
|
app.post('/api/whatsapp/send', requireAuth, async (req, res) => {
|
||||||
|
const { to, message, account } = req.body as Record<string, unknown>;
|
||||||
|
if (!to || !message) { res.status(400).json({ error: 'to and message are required' }); return; }
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('whatsapp_send_message', { to, message, account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/whatsapp/template', requireAuth, async (req, res) => {
|
||||||
|
const { to, template_name, language, components, account } = req.body as Record<string, unknown>;
|
||||||
|
if (!to || !template_name) { res.status(400).json({ error: 'to and template_name are required' }); return; }
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('whatsapp_send_template', { to, template_name, language, components, account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/whatsapp/templates', requireAuth, async (req, res) => {
|
||||||
|
const account = req.query.account as string | undefined;
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('whatsapp_list_templates', { account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── LinkedIn REST endpoints ─────────────────────────────────────
|
||||||
|
app.get('/api/linkedin/profile', requireAuth, async (req, res) => {
|
||||||
|
const account = req.query.account as string | undefined;
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('linkedin_get_profile', { account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/linkedin/post', requireAuth, async (req, res) => {
|
||||||
|
const { text, visibility, account } = req.body as Record<string, unknown>;
|
||||||
|
if (!text) { res.status(400).json({ error: 'text is required' }); return; }
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('linkedin_create_post', { text, visibility, account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/linkedin/search-connections', requireAuth, async (req, res) => {
|
||||||
|
const { keywords, account } = req.body as Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('linkedin_search_connections', { keywords, account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/linkedin/message', requireAuth, async (req, res) => {
|
||||||
|
const { recipient_id, message, account } = req.body as Record<string, unknown>;
|
||||||
|
if (!recipient_id || !message) { res.status(400).json({ error: 'recipient_id and message are required' }); return; }
|
||||||
|
try {
|
||||||
|
const result = await handleToolCall('linkedin_send_message', { recipient_id, message, account });
|
||||||
|
res.json(parseToolResult(result));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/pilot-request', async (req, res) => {
|
app.post('/api/pilot-request', async (req, res) => {
|
||||||
const origin = req.get('origin');
|
const origin = req.get('origin');
|
||||||
if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) {
|
if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) {
|
||||||
|
|||||||
185
src/manifest.ts
185
src/manifest.ts
@@ -390,6 +390,183 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── WhatsApp Business API tools ───────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'whatsapp_send_message',
|
||||||
|
category: 'whatsapp',
|
||||||
|
description: 'Send a WhatsApp text message to a phone number',
|
||||||
|
when_to_use:
|
||||||
|
'User asks to send a WhatsApp message, text someone on WhatsApp, or notify via WhatsApp. Only works if the recipient has messaged you within the last 24 hours.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['to', 'message'],
|
||||||
|
properties: {
|
||||||
|
to: { type: 'string', description: 'Recipient phone number in international format (e.g. +1234567890)' },
|
||||||
|
message: { type: 'string', description: 'Message text to send' },
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean' },
|
||||||
|
message_id: { type: 'string', description: 'WhatsApp message ID' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ to: '+1234567890', message: 'Hello from Hermes', account: 'default' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'whatsapp_send_template',
|
||||||
|
category: 'whatsapp',
|
||||||
|
description: 'Send an approved WhatsApp template message',
|
||||||
|
when_to_use:
|
||||||
|
'User wants to send a structured notification or alert via an approved WhatsApp template. Required when outside the 24-hour customer-service window.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['to', 'template_name'],
|
||||||
|
properties: {
|
||||||
|
to: { type: 'string', description: 'Recipient phone number in international format' },
|
||||||
|
template_name: { type: 'string', description: 'Name of the approved WhatsApp template' },
|
||||||
|
language: { type: 'string', description: 'Template language code (default: "en")' },
|
||||||
|
components: { type: 'array', description: 'Template components (header, body, buttons) with parameters' },
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean' },
|
||||||
|
message_id: { type: 'string', description: 'WhatsApp message ID' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ to: '+1234567890', template_name: 'hello_world', language: 'en', account: 'default' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'whatsapp_list_templates',
|
||||||
|
category: 'whatsapp',
|
||||||
|
description: 'List all approved WhatsApp message templates for the business account',
|
||||||
|
when_to_use:
|
||||||
|
'User asks what WhatsApp templates are available or wants to know which templates can be sent.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
templates: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
language: { type: 'string' },
|
||||||
|
status: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ account: 'default' }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── LinkedIn tools ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'linkedin_get_profile',
|
||||||
|
category: 'linkedin',
|
||||||
|
description: 'Get the LinkedIn profile of the authenticated user',
|
||||||
|
when_to_use:
|
||||||
|
'User asks about their LinkedIn profile, name, headline, or wants to verify which LinkedIn account is connected.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'LinkedIn person ID (OpenID sub)' },
|
||||||
|
firstName: { type: 'string' },
|
||||||
|
lastName: { type: 'string' },
|
||||||
|
email: { type: 'string' },
|
||||||
|
picture: { type: 'string', description: 'Profile photo URL' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ account: 'default' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin_create_post',
|
||||||
|
category: 'linkedin',
|
||||||
|
description: 'Create a post on LinkedIn',
|
||||||
|
when_to_use:
|
||||||
|
'User wants to publish an update, share content, or post to their LinkedIn feed.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['text'],
|
||||||
|
properties: {
|
||||||
|
text: { type: 'string', description: 'Post content text' },
|
||||||
|
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'PUBLIC (anyone) or CONNECTIONS (1st degree only)' },
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean' },
|
||||||
|
post_id: { type: 'string' },
|
||||||
|
url: { type: 'string', description: 'Direct link to the post' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ text: 'Excited to share our latest product update!', visibility: 'PUBLIC', account: 'default' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin_search_connections',
|
||||||
|
category: 'linkedin',
|
||||||
|
description: 'Search LinkedIn connections [REQUIRES PARTNERSHIP]',
|
||||||
|
when_to_use:
|
||||||
|
'User wants to search their LinkedIn network. NOTE: Requires LinkedIn Partnership Program — public API access was removed.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
keywords: { type: 'string', description: 'Search keywords' },
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string', description: 'Error or guidance message' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ keywords: 'software engineer', account: 'default' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin_send_message',
|
||||||
|
category: 'linkedin',
|
||||||
|
description: 'Send a direct message on LinkedIn [REQUIRES PARTNERSHIP]',
|
||||||
|
when_to_use:
|
||||||
|
'User wants to send a LinkedIn DM. NOTE: Requires LinkedIn Partnership Program — messaging is not available through the public API.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['recipient_id', 'message'],
|
||||||
|
properties: {
|
||||||
|
recipient_id: { type: 'string', description: 'LinkedIn person URN or ID' },
|
||||||
|
message: { type: 'string', description: 'Message text' },
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string', description: 'Error or guidance message' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
examples: [{ recipient_id: 'urn:li:person:abc123', message: 'Hi, thanks for connecting!', account: 'default' }],
|
||||||
|
},
|
||||||
|
|
||||||
// ── Obsidian tools ──────────────────────────────────────────────────────
|
// ── Obsidian tools ──────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
name: 'obsidian_search_notes',
|
name: 'obsidian_search_notes',
|
||||||
@@ -549,6 +726,14 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
|
|||||||
description: 'Email operations for Yahoo, FetcherPay, and Gmail accounts',
|
description: 'Email operations for Yahoo, FetcherPay, and Gmail accounts',
|
||||||
icon: '📧',
|
icon: '📧',
|
||||||
},
|
},
|
||||||
|
whatsapp: {
|
||||||
|
description: 'WhatsApp Business API messaging via Meta Cloud API',
|
||||||
|
icon: '💬',
|
||||||
|
},
|
||||||
|
linkedin: {
|
||||||
|
description: 'LinkedIn profile and posting via LinkedIn API',
|
||||||
|
icon: '🔗',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
172
src/tools.ts
172
src/tools.ts
@@ -2,6 +2,8 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|||||||
import { searchMessages, readMessage, getProfile, listFolders, type Account } from './imap.js';
|
import { searchMessages, readMessage, getProfile, listFolders, type Account } from './imap.js';
|
||||||
import { sendEmail, createDraft } from './smtp.js';
|
import { sendEmail, createDraft } from './smtp.js';
|
||||||
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js';
|
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js';
|
||||||
|
import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './clients/whatsapp.js';
|
||||||
|
import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js';
|
||||||
|
|
||||||
const ACCOUNT_PARAM = {
|
const ACCOUNT_PARAM = {
|
||||||
account: {
|
account: {
|
||||||
@@ -174,6 +176,114 @@ export const tools: Tool[] = [
|
|||||||
properties: {},
|
properties: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── WhatsApp Business API tools ────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'whatsapp_send_message',
|
||||||
|
description:
|
||||||
|
'Send a WhatsApp message to a phone number. Use when the user asks to send a WhatsApp message, text someone on WhatsApp, or notify via WhatsApp.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
to: { type: 'string', description: 'Recipient phone number in international format (e.g. +1234567890)' },
|
||||||
|
message: { type: 'string', description: 'Message text to send' },
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
required: ['to', 'message'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'whatsapp_send_template',
|
||||||
|
description:
|
||||||
|
'Send a WhatsApp template message (for approved templates). Use when sending structured notifications or alerts via WhatsApp templates.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
to: { type: 'string', description: 'Recipient phone number in international format' },
|
||||||
|
template_name: { type: 'string', description: 'Name of the approved WhatsApp template' },
|
||||||
|
language: { type: 'string', description: 'Template language code (default: "en")' },
|
||||||
|
components: { type: 'array', description: 'Template components (header, body, buttons) with parameters' },
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
required: ['to', 'template_name'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'whatsapp_get_message_status',
|
||||||
|
description:
|
||||||
|
'[DEPRECATED] Meta Cloud API does not support polling message status. Status updates are only available via webhooks.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message_id: { type: 'string', description: 'WhatsApp message ID (not used - webhook required)' },
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'whatsapp_list_templates',
|
||||||
|
description:
|
||||||
|
'List all approved WhatsApp message templates for the business account. Use when the user asks what templates are available.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── LinkedIn tools ─────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'linkedin_get_profile',
|
||||||
|
description:
|
||||||
|
'Get the LinkedIn profile of the authenticated user. Use when the user asks about their LinkedIn profile, name, or headline.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin_create_post',
|
||||||
|
description:
|
||||||
|
'Create a post on LinkedIn. Use when the user wants to publish an update, article, or share content on LinkedIn.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
text: { type: 'string', description: 'Post content text' },
|
||||||
|
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'Visibility: PUBLIC (anyone) or CONNECTIONS (1st degree only). Default: PUBLIC' },
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
required: ['text'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin_search_connections',
|
||||||
|
description:
|
||||||
|
'Search LinkedIn connections. [REQUIRES PARTNERSHIP] LinkedIn removed public API access to connections. This tool will guide you to apply for the Partnership Program.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
keywords: { type: 'string', description: 'Search keywords for connections' },
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin_send_message',
|
||||||
|
description:
|
||||||
|
'Send a direct message on LinkedIn. [REQUIRES PARTNERSHIP] LinkedIn messaging is not available through the public API. This tool will guide you to apply for the Partnership Program.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
recipient_id: { type: 'string', description: 'LinkedIn person URN or ID of the recipient' },
|
||||||
|
message: { type: 'string', description: 'Message text to send' },
|
||||||
|
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
|
||||||
|
},
|
||||||
|
required: ['recipient_id', 'message'],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function acct(args: Record<string, unknown>): Account {
|
function acct(args: Record<string, unknown>): Account {
|
||||||
@@ -245,6 +355,68 @@ export async function handleToolCall(
|
|||||||
result = await getSyncStatus();
|
result = await getSyncStatus();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ── WhatsApp Business API ──────────────────────────────────
|
||||||
|
case 'whatsapp_send_message':
|
||||||
|
result = await sendMessage({
|
||||||
|
to: args.to as string,
|
||||||
|
message: args.message as string,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'whatsapp_send_template':
|
||||||
|
result = await sendTemplate({
|
||||||
|
to: args.to as string,
|
||||||
|
template_name: args.template_name as string,
|
||||||
|
language: args.language as string | undefined,
|
||||||
|
components: args.components as unknown[] | undefined,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'whatsapp_get_message_status':
|
||||||
|
result = await getMessageStatus({
|
||||||
|
message_id: args.message_id as string,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'whatsapp_list_templates':
|
||||||
|
result = await listTemplates({
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ── LinkedIn ───────────────────────────────────────────────
|
||||||
|
case 'linkedin_get_profile':
|
||||||
|
result = await getLinkedInProfile({
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'linkedin_create_post':
|
||||||
|
result = await createLinkedInPost({
|
||||||
|
text: args.text as string,
|
||||||
|
visibility: (args.visibility as 'PUBLIC' | 'CONNECTIONS') ?? 'PUBLIC',
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'linkedin_search_connections':
|
||||||
|
result = await searchConnections({
|
||||||
|
keywords: args.keywords as string | undefined,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'linkedin_send_message':
|
||||||
|
result = await sendLinkedInMessage({
|
||||||
|
recipient_id: args.recipient_id as string,
|
||||||
|
message: args.message as string,
|
||||||
|
account: args.account as string | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
|
||||||
case 'yahoo_get_profile':
|
case 'yahoo_get_profile':
|
||||||
result = await getProfile('yahoo');
|
result = await getProfile('yahoo');
|
||||||
|
|||||||
Reference in New Issue
Block a user