Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3ee7051
Add Lobster Edition features: HTTP API server, OpenClaw integration, …
Mar 2, 2026
8a5634a
Add HTTP API server (src/http.ts) - enables LM Studio MCP connection …
Mar 2, 2026
86751ec
Update README - add Lobster Edition features: HTTP API, LM Studio MCP…
Mar 2, 2026
1db137b
Merge upstream/cloudweaver/clawdwatch - keep Lobster Edition HTTP API
Mar 2, 2026
e7a8df2
Add global coverage + news sources from upstream cloudweaver/clawdwatch
Mar 3, 2026
5299329
Update README - add MCP, HTTP API, global coverage docs
Mar 3, 2026
a0368fa
Fix region bounds - use upstream cloudweaver coords
Mar 3, 2026
2c55c6e
Add rate limiting with retry logic for OpenSky API
Mar 3, 2026
d5648fa
Add AgentMail email integration
Mar 3, 2026
5faad6f
Increase rate limiting: 30s interval, 10min cache for OpenSky free tier
Mar 3, 2026
6279591
Update README - add AgentMail, rate limiting docs
Mar 3, 2026
01d868a
Fix README header with status table
Mar 3, 2026
2692e34
Add memory/2026-03-03.md
Mar 3, 2026
c0a627e
Remove AgentMail from header
Mar 3, 2026
bfa9a53
Remove AgentMail - keeping it separate from Clawdwatch
Mar 3, 2026
2c5c4bf
Add ClawdWatch and AgentMail MCP servers for LM Studio
Mar 3, 2026
d892acc
Add clawdwatch_flight and clawdwatch_osint tool aliases
Mar 3, 2026
25e365c
Fix AgentMail API - use correct client.inboxes.messages path
Mar 3, 2026
c8233e8
Remove AgentMail MCP - keeping as separate project
Mar 3, 2026
5fc15b3
Add 5s timeout to MCP tool calls to prevent hanging
Mar 3, 2026
14c7cd1
v2.0: Global coverage + RSS news + disaster/weather intel
Jun 4, 2026
109f4ae
v2.1: Add /defcon endpoint + DEFCON in /osint and /snapshot
Jun 4, 2026
518b3dd
Add Hacker News RSS feed to tech region
Jun 14, 2026
a9542b5
v2.2 OSIRIS integration: sanctions, crypto, fires, CVE, WHOIS, DNS, T…
Jun 22, 2026
ad03c17
v2.3: Add 6 OSIRIS features (space-weather, sentinel, satellites, cyb…
Jun 22, 2026
b0ebcd2
v2.4: Add RECON Toolkit — SSL/TLS inspector, live news broadcasts, OF…
Jun 22, 2026
3943785
README: full v2.4 overhaul — provenance, RECON toolkit, toggle docs, …
Jun 22, 2026
3094b75
Bump version string in HTTP responses to 2.4.0-lobster
Jun 22, 2026
9df8283
MCP: auto-sync tool catalog from live HTTP server (6 → 37 tools)
Jun 22, 2026
7c3174e
v2.5: regions expansion (44→62) + CLI rewrite + OpenSanctions docs
Jun 22, 2026
79770e5
feat: add DEFCON alert handler, threat scoring, /defcon/score endpoin…
Jun 29, 2026
1d50179
v0.3.0: lock canonical brief format into SKILL.md and update brief sc…
Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 69 additions & 17 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,23 +1,75 @@
# Clawdwatch Configuration
# ClawdWatch Lobster Edition — environment configuration
# Copy to .env and fill in any keys you have. All keys are OPTIONAL —
# the server runs without any of them. Keys unlock additional endpoints.

# Region to monitor: middle_east | europe | usa | asia
WATCH_REGION=middle_east
# ============================================================
# Required for nothing — all endpoints work with sensible fallbacks
# ============================================================

# Telegram Alerts (get token from @BotFather)
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
# ============================================================
# Optional: enrich /crypto/btc responses with fee estimates
# ============================================================
# MEMPOOL_SPACE_URL=https://mempool.space

# Twitter/X API (optional - for social monitoring)
TWITTER_BEARER_TOKEN=your_bearer_token_here
# ============================================================
# Optional: enrich /sanctions with full OFAC SDN mirror
# ============================================================
# Free hosted API key from https://opensanctions.org/api/
# 1. Sign up at https://opensanctions.org/ (free account)
# 2. Open the API dashboard: https://opensanctions.org/api/
# 3. Click "Generate API key" — copy the 40-char string
# 4. Paste it here as OPENSANCTIONS_API_KEY=<your-key>
#
# Without a key:
# - /sanctions?q= returns 401 (endpoint is feature-toggled off)
# - /ofac/check returns { matched: false, reason: 'OPENSANCTIONS_API_KEY not configured' }
# - /whois, /geo, /crypto/btc, /crypto/eth responses will have ofac_sanctioned: null
#
# LOCAL ON-PREMISE ALTERNATIVE (no key needed, runs entirely offline):
# The OpenSanctions full dataset can be downloaded and indexed locally.
# See: https://www.opensanctions.org/docs/on-premise/
# Two paths:
# A) Bulk dataset download (CSV/JSON) → load into local SQLite/Postgres
# $ pip install opensanctions
# $ opensanctions build datasets.yml (downloads + indexes ~6GB)
# B) Run the yente API server locally (the same API OpenSanctions hosts):
# $ docker run -p 8000:8000 \
# -e YENTE_API_KEY=anything \
# -e YENTE_DATABASE_URI=sqlite:///opensanctions.db \
# ghcr.io/opensanctions/yente:latest
# Then set OPENSANCTIONS_BASE_URL=http://localhost:8000 in this .env
# (and adjust OSIRIS sanctionsLookup() in src/sources/osiris.ts to point
# at your local yente instead of https://api.opensanctions.org)
#
# The on-prem path is heavier to set up but gives you:
# - Zero rate limits
# - Full historical snapshot (not just current SDN list)
# - Air-gapped operation (good for sensitive investigations)
#
OPENSANCTIONS_API_KEY=
# OPENSANCTIONS_BASE_URL=https://api.opensanctions.org # override for self-hosted

# Sentinel Hub (optional - for satellite imagery)
# Register at: https://www.sentinel-hub.com/
SENTINEL_HUB_CLIENT_ID=
SENTINEL_HUB_CLIENT_SECRET=

# AIS Stream (optional - for ship tracking)
# Register at: https://aisstream.io/
AISSTREAM_API_KEY=
# ============================================================
# Optional: enable /fires (NASA FIRMS active fire hotspots)
# ============================================================
# Free MAP_KEY from https://firms.modaps.eosdis.nasa.gov/api/area/
# SIGN UP → request key → paste here
# Without this, /fires returns "Invalid MAP_KEY" from NASA FIRMS.
FIRMS_MAP_KEY=

# Discord Alerts (coming soon)
DISCORD_WEBHOOK_URL=
# ============================================================
# Optional: enrich /defcon with custom source
# ============================================================
# DEFCON_SOURCE_URL=https://defconlevel.com

# ============================================================
# Optional: TCP port scanner (/scan endpoint)
# ============================================================
# WARNING: port scanning third-party hosts may violate their TOS or local law.
# Off by default. Set to 'true' to enable /scan endpoint.
# The scanner has SSRF guards — private IPs (10.x, 192.168.x, 172.16-31.x,
# 127.x, 169.254.x, ::1, fc00::/7, fe80::/10, 224.x) are blocked unless
# PORT_SCAN_ALLOW_PRIVATE is also set to true.
PORT_SCAN_ENABLED=false
# PORT_SCAN_ALLOW_PRIVATE=false
537 changes: 393 additions & 144 deletions README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions mcp-clawdwatch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
*.log
.DS_Store
179 changes: 179 additions & 0 deletions mcp-clawdwatch/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env node

// ClawdWatch MCP Server - stdio transport for LM Studio
// Auto-syncs with the live ClawdWatch HTTP API at startup by fetching /
// so the tool catalog always matches whatever the server exposes.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const BASE_URL = process.env.CLAWDWATCH_URL || 'http://localhost:3444';
const TOOL_PREFIX = 'clawdwatch';

// ---------------------------------------------------------------------------
// Parse a catalog entry string into path + method + params
// "GET /ssl/:host?port= (TLS cert chain + expiry, free)"
// "POST /ofac/refresh (reload OFAC cache from OpenSanctions)"
// ---------------------------------------------------------------------------
function parseEndpoint(entry) {
// Trim description in parens
const stripped = entry.replace(/\s*\([^)]*\)\s*$/, '').trim();
// Split method + rest
const [method, ...rest] = stripped.split(/\s+/);
const pathAndQuery = rest.join(' ');
const [pathPart, queryPart] = pathAndQuery.split('?');

const pathParams = [];
const re = /:([a-zA-Z_][a-zA-Z0-9_]*)/g;
let m;
while ((m = re.exec(pathPart)) !== null) pathParams.push(m[1]);

const queryParams = [];
if (queryPart) {
for (const q of queryPart.split('&')) {
const [name, defaultVal] = q.split('=');
if (name) queryParams.push({ name, default: defaultVal ?? '' });
}
}

return {
method: (method || 'GET').toUpperCase(),
pathTemplate: pathPart,
pathParams,
queryParams,
};
}

// Build input schema properties from parsed endpoint
function buildInputSchema(parsed) {
const props = {};
const required = [];
for (const p of parsed.pathParams) {
props[p] = { type: 'string', description: `Path param: ${p}` };
required.push(p);
}
for (const q of parsed.queryParams) {
props[q.name] = {
type: q.name === 'days' || q.name === 'hours' || q.name === 'limit' || q.name === 'min' || q.name === 'radius' || q.name === 'port' || q.name === 'lat' || q.name === 'lng' ? 'number' : 'string',
description: `Query param: ${q.name}`,
};
}
return {
type: 'object',
properties: props,
...(required.length > 0 ? { required } : {}),
};
}

// Substitute path params and append query string
function buildUrl(template, args = {}, queryDefaults = []) {
let path = template;
for (const [k, v] of Object.entries(args)) {
path = path.replace(`:${k}`, encodeURIComponent(String(v)));
}
const qp = new URLSearchParams();
for (const q of queryDefaults) {
if (args[q.name] !== undefined && args[q.name] !== '') {
qp.set(q.name, String(args[q.name]));
} else if (q.default) {
qp.set(q.name, q.default);
}
}
const qs = qp.toString();
return qs ? `${BASE_URL}${path}?${qs}` : `${BASE_URL}${path}`;
}

// ---------------------------------------------------------------------------
// Fetch the live catalog and build tool list
// ---------------------------------------------------------------------------
async function fetchCatalog() {
try {
const r = await fetch(`${BASE_URL}/`, { signal: AbortSignal.timeout(5000) });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
return data.endpoints || {};
} catch (e) {
console.error(`[clawdwatch-mcp] failed to fetch catalog from ${BASE_URL}: ${e.message}`);
return {};
}
}

function buildTools(catalog) {
const tools = [];
for (const [key, entry] of Object.entries(catalog)) {
const parsed = parseEndpoint(entry);
tools.push({
name: `${TOOL_PREFIX}_${key}`,
description: `${parsed.method} ${parsed.pathTemplate}${parsed.queryParams.length ? '?' + parsed.queryParams.map(q => q.name + (q.default ? '=' + q.default : '')).join('&') : ''}`,
inputSchema: buildInputSchema(parsed),
_parsed: parsed,
});
}
return tools;
}

// ---------------------------------------------------------------------------
// Server setup
// ---------------------------------------------------------------------------
const server = new Server({
name: 'clawdwatch',
version: '2.4.0-lobster'
}, {
capabilities: { tools: {} }
});

let tools = [];

server.setRequestHandler(ListToolsRequestSchema, async () => {
// Strip internal _parsed before returning to clients
return { tools: tools.map(({ _parsed, ...rest }) => rest) };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = tools.find(t => t.name === name);
if (!tool) {
return { content: [{ type: 'text', text: `Unknown tool: ${name}. Run tools/list to see available tools.` }] };
}
try {
const url = buildUrl(tool._parsed.pathTemplate, args, tool._parsed.queryParams);
const fetchOpts = { signal: AbortSignal.timeout(15000) };
if (tool._parsed.method === 'POST') {
fetchOpts.method = 'POST';
fetchOpts.headers = { 'Content-Type': 'application/json' };
}
const r = await fetch(url, fetchOpts);
const text = await r.text();
let body;
try { body = JSON.parse(text); } catch { body = text; }
return {
content: [{
type: 'text',
text: JSON.stringify({
ok: r.ok,
status: r.status,
url,
method: tool._parsed.method,
data: body,
}, null, 2),
}],
};
} catch (e) {
return { content: [{ type: 'text', text: `Error calling ${name}: ${e.message}` }] };
}
});

// ---------------------------------------------------------------------------
// Boot
// ---------------------------------------------------------------------------
console.error(`[clawdwatch-mcp] fetching catalog from ${BASE_URL}/ ...`);
const catalog = await fetchCatalog();
tools = buildTools(catalog);
console.error(`[clawdwatch-mcp] loaded ${tools.length} tools:`);
for (const t of tools) {
console.error(` - ${t.name} ${t._parsed.method} ${t._parsed.pathTemplate}`);
}

const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[clawdwatch-mcp] running on stdio');
Loading