Add Email (IMAP/SMTP) and SSH Remote Access connectors

- CLAUDE.md: developer guide for the marketplace repo
- email/: generic IMAP+SMTP connector (stdlib only, config via env)
  - env[] array, auth.type: password, verify.py (IMAP+SMTP probe)
- ssh/: SSH Remote Access connector ported from Skald, modified
  - ALIASES_FILE → ~/.ssh_aliases.json (was ./secrets/...)
  - optional env[] (TTL/timeout tunables)
  - auth.type: none (per-alias auth at runtime via elicitation)
- tavily/: restructured with env[] (tavilyApiKey), verify.py, auth block
- connectors.json: added email, ssh, and tavily verify.py entries
- SKALD.md: documented env[], verify, password auth, SSH connector
This commit is contained in:
2026-07-16 22:29:33 +01:00
parent dedd09d7c7
commit 1caba6946c
15 changed files with 3420 additions and 8 deletions
+131
View File
@@ -0,0 +1,131 @@
{
"id": "email",
"name": "Email (IMAP/SMTP)",
"version": "1.0.0",
"type": "mcp_local",
"launch_command": "python3 email_mcp_server.py",
"transport": "stdio",
"requires": [
"PYTHON",
"ENV"
],
"dependencies": [],
"env": [
{
"name": "EMAIL_IMAP_HOST",
"label": "IMAP host",
"description": "IMAP server hostname for reading mail (e.g. imap.gmail.com, outlook.office365.com, imap.mail.me.com).",
"required": true,
"secret": false,
"example": "imap.gmail.com"
},
{
"name": "EMAIL_IMAP_PORT",
"label": "IMAP port",
"description": "IMAP-over-SSL port. Almost always 993.",
"required": false,
"secret": false,
"default": "993",
"example": "993"
},
{
"name": "EMAIL_SMTP_HOST",
"label": "SMTP host",
"description": "SMTP server hostname for sending mail (e.g. smtp.gmail.com, smtp.office365.com, smtp.mail.me.com).",
"required": true,
"secret": false,
"example": "smtp.gmail.com"
},
{
"name": "EMAIL_SMTP_PORT",
"label": "SMTP port",
"description": "SMTP port: 465 for implicit SSL, 587 for STARTTLS.",
"required": false,
"secret": false,
"default": "465",
"example": "465"
},
{
"name": "EMAIL_SMTP_SECURITY",
"label": "SMTP security",
"description": "How to secure the SMTP connection: 'ssl' (port 465), 'starttls' (port 587), or 'plain'. If omitted, inferred from the port.",
"required": false,
"secret": false,
"default": "ssl",
"example": "ssl"
},
{
"name": "EMAIL_USERNAME",
"label": "Username / email address",
"description": "Login username, usually the full email address.",
"required": true,
"secret": false,
"example": "me@example.com"
},
{
"name": "EMAIL_PASSWORD",
"label": "Password / app password",
"description": "Account password. Providers with 2FA (Gmail, iCloud, Yahoo, Outlook) require an app-specific password, not your normal login password.",
"required": true,
"secret": true,
"example": "abcd efgh ijkl mnop"
},
{
"name": "EMAIL_FROM",
"label": "From address",
"description": "Address to put in the From header when sending. Defaults to the username if omitted.",
"required": false,
"secret": false,
"example": "me@example.com"
}
],
"setup_instructions": [
"No files on disk and no OAuth: configuration is entirely via environment variables.",
"Set EMAIL_IMAP_HOST, EMAIL_SMTP_HOST, EMAIL_USERNAME and EMAIL_PASSWORD (required).",
"For providers with 2FA (Gmail, iCloud, Yahoo, Outlook), generate an app-specific password and use it as EMAIL_PASSWORD.",
"Optionally set EMAIL_IMAP_PORT (993), EMAIL_SMTP_PORT (465/587), EMAIL_SMTP_SECURITY (ssl/starttls) and EMAIL_FROM.",
"Run the 'status' tool to verify IMAP and SMTP both authenticate."
],
"docs": [
{
"lang": "en",
"description": "Generic email connector over IMAP + SMTP. Works with any provider (Gmail, Outlook/Office 365, iCloud, Yahoo, Fastmail, self-hosted, corporate) using standard protocols and an app password — no Google Cloud Console, no OAuth. Read, search, organise and send mail, download attachments, and receive real-time push notifications when new mail arrives (IMAP IDLE, with polling fallback). Standard library only, zero dependencies.",
"llm_short_description": "Email MCP server (IMAP/SMTP, any provider). Config via env vars. Tools: status, list_messages, get_message, get_thread, list_folders, modify_message, send_message, get_profile, create_folder, download_attachments. Emits event/new_email push notifications on new INBOX mail."
}
],
"mcp_config": {
"command": "python3",
"args": [
"email_mcp_server.py"
],
"env": {
"EMAIL_IMAP_HOST": "{ENV:EMAIL_IMAP_HOST}",
"EMAIL_IMAP_PORT": "{ENV:EMAIL_IMAP_PORT}",
"EMAIL_SMTP_HOST": "{ENV:EMAIL_SMTP_HOST}",
"EMAIL_SMTP_PORT": "{ENV:EMAIL_SMTP_PORT}",
"EMAIL_SMTP_SECURITY": "{ENV:EMAIL_SMTP_SECURITY}",
"EMAIL_USERNAME": "{ENV:EMAIL_USERNAME}",
"EMAIL_PASSWORD": "{SECRET:EMAIL_PASSWORD}",
"EMAIL_FROM": "{ENV:EMAIL_FROM}"
}
},
"verify": {
"command": "python3 verify.py",
"timeout_secs": 20
},
"homepage": "",
"icon_small": "icon_sm.svg",
"icon_large": "icon_lg.svg",
"scope": "user",
"tags": [
"email",
"mcp",
"local",
"imap",
"smtp"
],
"auth": {
"type": "password",
"delivery": "env"
}
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><rect width="96" height="96" rx="20" fill="#2563eb"/><rect x="20" y="30" width="56" height="36" rx="5" fill="#fff"/><path d="M22 34l26 18 26-18" fill="none" stroke="#2563eb" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 308 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><rect width="48" height="48" rx="10" fill="#2563eb"/><rect x="10" y="15" width="28" height="18" rx="3" fill="#fff"/><path d="M11 17l13 9 13-9" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 306 B

+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Verify the Email connector credentials by attempting IMAP and SMTP logins.
Reads the EMAIL_* environment variables (the same set the MCP server consumes),
opens one IMAP and one SMTP connection, attempts LOGIN on each, and prints a
single JSON object on stdout:
{"ok": true, "message": "...", "details": {"imap": "...", "smtp": "..."}}
{"ok": false, "message": "...", "details": {"imap": "...", "smtp": "..."}}
Exit code is 0 on success, 1 on any failure. No credentials are ever printed.
stdlib only (imaplib, smtplib, ssl) — mirrors the email_mcp_server.py constraint.
"""
import imaplib
import json
import os
import smtplib
import ssl
import sys
def _result(ok, message, **extra):
print(json.dumps({"ok": ok, "message": message, **extra}))
sys.exit(0 if ok else 1)
def _imap():
host = os.environ.get("EMAIL_IMAP_HOST", "").strip()
port = int(os.environ.get("EMAIL_IMAP_PORT", "993") or "993")
user = os.environ.get("EMAIL_USERNAME", "").strip()
pw = os.environ.get("EMAIL_PASSWORD", "")
if not host or not user or not pw:
return False, "EMAIL_IMAP_HOST, EMAIL_USERNAME and EMAIL_PASSWORD are required"
try:
ctx = ssl.create_default_context()
with imaplib.IMAP4_SSL(host, port, ssl_context=ctx) as imap:
imap.login(user, pw)
imap.select("INBOX", readonly=True)
return True, f"IMAP login to {host}:{port} successful"
except Exception as e:
return False, f"IMAP login to {host}:{port} failed: {e}"
def _smtp():
host = os.environ.get("EMAIL_SMTP_HOST", "").strip()
port = int(os.environ.get("EMAIL_SMTP_PORT", "465") or "465")
sec = os.environ.get("EMAIL_SMTP_SECURITY", "").strip().lower()
user = os.environ.get("EMAIL_USERNAME", "").strip()
pw = os.environ.get("EMAIL_PASSWORD", "")
if not host or not user or not pw:
return False, "EMAIL_SMTP_HOST, EMAIL_USERNAME and EMAIL_PASSWORD are required"
if not sec:
sec = "starttls" if port in (587, 25) else "ssl"
try:
ctx = ssl.create_default_context()
if sec == "ssl":
with smtplib.SMTP_SSL(host, port, context=ctx, timeout=15) as s:
s.login(user, pw)
elif sec == "starttls":
with smtplib.SMTP(host, port, timeout=15) as s:
s.starttls(context=ctx)
s.login(user, pw)
else:
with smtplib.SMTP(host, port, timeout=15) as s:
s.login(user, pw)
return True, f"SMTP login to {host}:{port} ({sec}) successful"
except Exception as e:
return False, f"SMTP login to {host}:{port} failed: {e}"
def main():
imap_ok, imap_msg = _imap()
smtp_ok, smtp_msg = _smtp()
if imap_ok and smtp_ok:
_result(True, "IMAP and SMTP authentication successful",
details={"imap": imap_msg, "smtp": smtp_msg})
if imap_ok and not smtp_ok:
_result(False, f"IMAP ok but SMTP failed: {smtp_msg}",
details={"imap": imap_msg, "smtp": smtp_msg})
if smtp_ok and not imap_ok:
_result(False, f"SMTP ok but IMAP failed: {imap_msg}",
details={"imap": imap_msg, "smtp": smtp_msg})
_result(False, f"Both IMAP and SMTP failed: {imap_msg} | {smtp_msg}",
details={"imap": imap_msg, "smtp": smtp_msg})
if __name__ == "__main__":
main()