closecadence-mcp 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/index.js +122 -6
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@ Use your **Close Cadence** real-estate tools from your own Claude. This is a tin
|
|
|
4
4
|
Claude (Desktop / Code) call your Close Cadence tools — listings, CMAs, social, follow-ups, flyers,
|
|
5
5
|
photo enhancement — directly, in your saved voice, on your plan's credits.
|
|
6
6
|
|
|
7
|
-
It's a thin client: it only forwards your authenticated requests to `closecadence.
|
|
7
|
+
It's a thin client: it only forwards your authenticated requests to `tools.closecadence.com`. No tool logic or
|
|
8
8
|
data lives in this package.
|
|
9
9
|
|
|
10
10
|
## Setup (about 1 minute)
|
|
11
|
-
1. Get your key in the portal → **
|
|
11
|
+
1. Get your key in the portal → **Use in Claude** (format `cck.cus_….…`).
|
|
12
12
|
2. Add this to your Claude config (Claude Desktop → Settings → Developer → Edit Config):
|
|
13
13
|
|
|
14
14
|
```json
|
|
@@ -28,6 +28,7 @@ data lives in this package.
|
|
|
28
28
|
- `cc_listing`, `cc_cma`, `cc_social`, `cc_email`, `cc_followup`, `cc_market_report`, `cc_open_house`, `cc_prospecting`, `cc_listing_presentation`
|
|
29
29
|
- `cc_comps`, `cc_property` (property data) · `cc_flyer`, `cc_enhance` (media)
|
|
30
30
|
- `cc_list_tools` (what's on your plan) · `cc_run` (any tool by id)
|
|
31
|
+
- Partner-spec aliases (2026-06-23 connector spec): `generate_listing_doc` (= `cc_listing`), `generate_cma` (= `cc_cma`), `create_custom_video` (listing-tour **video script/storyboard**; rendered video pending Creatomate/HeyGen keys), `list_assets` (your recent generated outputs), `job_status` (poll an async job by id)
|
|
31
32
|
|
|
32
33
|
If your membership lapses or you run out of credits, the tools say so clearly and point you to the portal.
|
|
33
34
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Close Cadence MCP server — lets an agent's own Claude call the Close Cadence tools.
|
|
3
|
-
// THIN CLIENT ONLY: it proxies authenticated calls to the licensed API at closecadence.
|
|
3
|
+
// THIN CLIENT ONLY: it proxies authenticated calls to the licensed API at tools.closecadence.com. No business
|
|
4
4
|
// logic, prompts, or comp/CMA data live here (that's protected, server-side IP) — this just forwards the
|
|
5
5
|
// agent's key + inputs and returns the result. Auth: env CC_API_KEY (cck.cus_xxx.yyy). stdio transport.
|
|
6
6
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { z } from 'zod';
|
|
9
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
9
12
|
|
|
10
|
-
const API = process.env.CC_API_BASE || 'https://closecadence.
|
|
13
|
+
const API = process.env.CC_API_BASE || 'https://tools.closecadence.com';
|
|
11
14
|
const KEY = process.env.CC_API_KEY || '';
|
|
12
15
|
|
|
16
|
+
// ---- Pillar 2: brand + market profiles travel with every job ----
|
|
17
|
+
// The connector is the bridge, so it carries the realtor's brand_profile.json + market_profile.json to
|
|
18
|
+
// the platform on EVERY tool call — outputs come back on-brand AND on-market without the agent having to
|
|
19
|
+
// remember to attach them. Looked up (in order): CC_PROFILE_DIR, the CloseCadence data dir, the cwd.
|
|
20
|
+
// Missing/oversized/invalid files are silently skipped (the server also has a per-member fallback copy).
|
|
21
|
+
function ccDataDir() {
|
|
22
|
+
if (process.platform === 'win32') return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'CloseCadence');
|
|
23
|
+
if (process.platform === 'darwin') return join(homedir(), 'Library', 'Application Support', 'CloseCadence');
|
|
24
|
+
return join(homedir(), '.config', 'CloseCadence');
|
|
25
|
+
}
|
|
26
|
+
function readJsonProfile(name) {
|
|
27
|
+
const dirs = [process.env.CC_PROFILE_DIR, ccDataDir(), process.cwd()].filter(Boolean);
|
|
28
|
+
for (const d of dirs) {
|
|
29
|
+
try {
|
|
30
|
+
const p = join(d, name);
|
|
31
|
+
if (!existsSync(p)) continue;
|
|
32
|
+
const raw = readFileSync(p, 'utf8');
|
|
33
|
+
if (raw.length > 96 * 1024) continue; // guard against an oversized file blowing the request
|
|
34
|
+
const obj = JSON.parse(raw);
|
|
35
|
+
if (obj && typeof obj === 'object') return obj;
|
|
36
|
+
} catch { /* unreadable / bad JSON -> skip */ }
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function loadProfiles() {
|
|
41
|
+
const out = {};
|
|
42
|
+
const bp = readJsonProfile('brand_profile.json'); if (bp) out.brand_profile = bp;
|
|
43
|
+
const mp = readJsonProfile('market_profile.json'); if (mp) out.market_profile = mp;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
const errText = (m) => ({ content: [{ type: 'text', text: m }], isError: true });
|
|
14
48
|
|
|
15
49
|
// Turn an API response into MCP content, surfacing license/credit problems clearly.
|
|
16
50
|
async function handle(r) {
|
|
17
51
|
let j = {};
|
|
18
52
|
try { j = await r.json(); } catch { /* non-JSON */ }
|
|
19
|
-
if (r.status === 401) return errText('Your Close Cadence membership isn’t active, or the key is invalid. Renew or rotate your key in the portal (closecadence.
|
|
20
|
-
if (r.status === 402) return errText((j && j.error) || 'You’re out of credits for this month. Top up at closecadence.
|
|
53
|
+
if (r.status === 401) return errText('Your Close Cadence membership isn’t active, or the key is invalid. Renew or rotate your key in the portal (tools.closecadence.com/portal → Use in Claude).');
|
|
54
|
+
if (r.status === 402) return errText((j && j.error) || 'You’re out of credits for this month. Top up at tools.closecadence.com/portal → Credits (or turn on auto-refill).');
|
|
21
55
|
if (r.status === 429) return errText((j && j.error) || 'Rate limited, or this key was flagged for use from too many places. Try again shortly, or rotate it in the portal.');
|
|
22
56
|
if (r.status === 503) return errText((j && j.error) || 'This tool is being set up — check back shortly.');
|
|
23
57
|
if (!r.ok || (j && j.error)) return errText((j && j.error) || ('Close Cadence error ' + r.status + '.'));
|
|
@@ -26,12 +60,12 @@ async function handle(r) {
|
|
|
26
60
|
}
|
|
27
61
|
|
|
28
62
|
function requireKey() {
|
|
29
|
-
return KEY ? null : errText('No CC_API_KEY set. Add your Close Cadence key (portal →
|
|
63
|
+
return KEY ? null : errText('No CC_API_KEY set. Add your Close Cadence key (portal → Use in Claude) to the MCP config "env", e.g. { "CC_API_KEY": "cck...." }.');
|
|
30
64
|
}
|
|
31
65
|
async function callTool(tool, inputs) {
|
|
32
66
|
const miss = requireKey(); if (miss) return miss;
|
|
33
67
|
try {
|
|
34
|
-
const r = await fetch(API + '/api/tool', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': KEY }, body: JSON.stringify({ tool, inputs: inputs || {} }) });
|
|
68
|
+
const r = await fetch(API + '/api/tool', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': KEY }, body: JSON.stringify({ tool, inputs: inputs || {}, ...loadProfiles() }) });
|
|
35
69
|
return await handle(r);
|
|
36
70
|
} catch (e) { return errText('Could not reach Close Cadence: ' + String(e && e.message || e)); }
|
|
37
71
|
}
|
|
@@ -42,6 +76,13 @@ async function callPath(path, body) {
|
|
|
42
76
|
return await handle(r);
|
|
43
77
|
} catch (e) { return errText('Could not reach Close Cadence: ' + String(e && e.message || e)); }
|
|
44
78
|
}
|
|
79
|
+
async function getPath(path) {
|
|
80
|
+
const miss = requireKey(); if (miss) return miss;
|
|
81
|
+
try {
|
|
82
|
+
const r = await fetch(API + path, { headers: { 'x-api-key': KEY } });
|
|
83
|
+
return await handle(r);
|
|
84
|
+
} catch (e) { return errText('Could not reach Close Cadence: ' + String(e && e.message || e)); }
|
|
85
|
+
}
|
|
45
86
|
|
|
46
87
|
const server = new McpServer({ name: 'closecadence', version: '0.1.0' });
|
|
47
88
|
|
|
@@ -57,6 +98,7 @@ const NAMED = [
|
|
|
57
98
|
['cc_open_house', 'open-house-kit', 'Open-house kit: social + email + neighbor invite + visitor follow-up.', 'address, details'],
|
|
58
99
|
['cc_prospecting', 'prospecting', 'Expired / FSBO outreach: letter + call script + texts.', 'type, context'],
|
|
59
100
|
['cc_listing_presentation', 'listing-presentation', 'Win-the-listing presentation content, slide by slide.', 'address, property, seller_goal, market, differentiators'],
|
|
101
|
+
['cc_avatar_report', 'avatar-marketing-report', 'Two-presenter AI-avatar marketing report script from your numbers (Studio) — feeds the avatar video renderer.', 'agent_name, co_presenter, market, period, total_volume, sales_count, avg_price, top_sale, wins, focus'],
|
|
60
102
|
];
|
|
61
103
|
for (const [name, id, desc, fields] of NAMED) {
|
|
62
104
|
server.registerTool(
|
|
@@ -77,6 +119,12 @@ server.registerTool('cc_property', { title: 'cc_property', description: 'Propert
|
|
|
77
119
|
server.registerTool('cc_flyer', { title: 'cc_flyer', description: 'Generate a "just listed" card / flyer image (readable text).', inputSchema: { prompt: z.string().optional(), headline: z.string().optional() } }, async (a) => callPath('/api/media/flyer', a));
|
|
78
120
|
server.registerTool('cc_enhance', { title: 'cc_enhance', description: 'Enhance a listing photo (sky replacement / HDR / declutter).', inputSchema: { imageUrl: z.string().url() } }, async (a) => callPath('/api/media/enhance', a));
|
|
79
121
|
|
|
122
|
+
// Avatar Studio (Studio tier): generate a branded talking-head marketing VIDEO or REEL in your own
|
|
123
|
+
// voice from your numbers. Rendering is async — cc_avatar_render enqueues + returns a job id; poll
|
|
124
|
+
// cc_avatar_status for the finished video url. Each render uses plan credits (video 500 / reel 120).
|
|
125
|
+
server.registerTool('cc_avatar_render', { title: 'cc_avatar_render', description: 'Generate an on-brand AI-avatar marketing VIDEO or REEL in your voice from your numbers (Studio). Async — returns a job id; poll cc_avatar_status. Uses plan credits.', inputSchema: { kind: z.enum(['report', 'reel']).optional().describe('report (full) or reel (short)'), fmt: z.enum(['video', 'reel']).optional().describe('video = 16:9, reel = 9:16') } }, async ({ kind, fmt }) => callPath('/api/avatar/request', { kind: kind || 'report', fmt: fmt || (kind === 'reel' ? 'reel' : 'video') }));
|
|
126
|
+
server.registerTool('cc_avatar_status', { title: 'cc_avatar_status', description: 'Check an avatar render job by id (or omit to list your recent renders). Returns status + the finished video url when ready.', inputSchema: { job: z.string().optional().describe('job id from cc_avatar_render') } }, async ({ job }) => getPath('/api/avatar/status' + (job ? ('?job=' + encodeURIComponent(job)) : '')));
|
|
127
|
+
|
|
80
128
|
// Discovery + escape hatch.
|
|
81
129
|
server.registerTool('cc_list_tools', { title: 'cc_list_tools', description: 'List every Close Cadence tool available to your plan.', inputSchema: {} }, async () => {
|
|
82
130
|
const miss = requireKey(); if (miss) return miss;
|
|
@@ -85,4 +133,72 @@ server.registerTool('cc_list_tools', { title: 'cc_list_tools', description: 'Lis
|
|
|
85
133
|
});
|
|
86
134
|
server.registerTool('cc_run', { title: 'cc_run', description: 'Run ANY Close Cadence tool by id with an inputs object. Prefer the specific cc_* tools when one fits.', inputSchema: { tool: z.string().describe('Tool id, e.g. bio-builder'), inputs: z.record(z.any()).optional() } }, async ({ tool, inputs }) => callTool(tool, inputs));
|
|
87
135
|
|
|
136
|
+
// Local-only: write the live Command Center dashboard board. No key/subscription needed — the dashboard
|
|
137
|
+
// renders purely from this local file (the permanent asset) even if the AI subscription lapses. Stays on
|
|
138
|
+
// the agent's device. Mirrors the desktop host's cc_save_board (board.json in the CloseCadence data dir).
|
|
139
|
+
function boardPath() {
|
|
140
|
+
let dir;
|
|
141
|
+
if (process.platform === 'win32') dir = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'CloseCadence');
|
|
142
|
+
else if (process.platform === 'darwin') dir = join(homedir(), 'Library', 'Application Support', 'CloseCadence');
|
|
143
|
+
else dir = join(homedir(), '.config', 'CloseCadence');
|
|
144
|
+
mkdirSync(dir, { recursive: true });
|
|
145
|
+
return join(dir, 'board.json');
|
|
146
|
+
}
|
|
147
|
+
server.registerTool('cc_save_board', {
|
|
148
|
+
title: 'cc_save_board',
|
|
149
|
+
description: "Update the agent's live Command Center dashboard. After reading their recent real-estate email, pass the full board object — it saves locally and the desktop dashboard refreshes. Stays on the agent's device. Core blocks: summary, transactions, tasks, email, signatures, notes, meta. Pillar-2/3 blocks the dashboard also renders: account { name, title, brokerage, photo_url, tier, tier_label, trial_ends, credits_remaining, credits_monthly, status } (pull from the cc account/credits endpoint so the identity + credits strip survive a lapse), assets [ { kind, title, status, status_detail, url, job_id, ts } ] (platform jobs — listing docs, videos, CMAs), roi { period_label, hours_saved, drafts_written, assets_produced, deals_tracked }, income, extra_panels.",
|
|
150
|
+
inputSchema: { board: z.record(z.any()).describe('The full board: { meta, summary, transactions, tasks, email, signatures, notes, income, account, assets, roi, extra_panels }.') },
|
|
151
|
+
annotations: { readOnlyHint: false, openWorldHint: false },
|
|
152
|
+
}, async ({ board }) => {
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(boardPath(), JSON.stringify(board ?? {}));
|
|
155
|
+
return { content: [{ type: 'text', text: 'Board saved — it’ll refresh in the Close Cadence Command Center dashboard.' }] };
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return errText('Couldn’t save the board: ' + String(e && e.message || e));
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---- Partner-spec aliases (2026-06-23 Platform Connector Architecture spec, Morgahn Grey) ----
|
|
162
|
+
// Thin, spec-named aliases over the EXISTING backend — additive, no breakage to the cc_* tools above.
|
|
163
|
+
// Single source of truth: each entry maps a spec name -> the same backend a cc_* tool already uses, so
|
|
164
|
+
// the mapping stays obvious and maintainable. Tool-engine aliases go to /api/tool (same id, same auth).
|
|
165
|
+
const SPEC_TOOL_ALIASES = [
|
|
166
|
+
// spec_name backend tool id (same backend as) description / inputs
|
|
167
|
+
['generate_listing_doc', 'listing-engine', 'Full listing marketing package: MLS remarks, long description, social posts, hooks (same engine as cc_listing).', 'address, price, beds, baths, sqft, features'],
|
|
168
|
+
['generate_cma', 'cma-narrative', 'CMA pricing narrative + objection handling from comps, for a seller (same engine as cc_cma; pair with cc_comps for real data).', 'address, comps, suggested_price, seller_expectation, market'],
|
|
169
|
+
// create_custom_video returns a production-ready VIDEO SCRIPT / STORYBOARD today (listing tour script).
|
|
170
|
+
// Rendered-video output (an actual .mp4 URL) is PENDING provider keys (Creatomate / HeyGen) — this tool
|
|
171
|
+
// does NOT return a video file or fake a URL; it returns the script the renderer will use once wired.
|
|
172
|
+
['create_custom_video', 'listing-tour', 'Production-ready VIDEO SCRIPT / STORYBOARD for a listing tour (hook, room-by-room narration with shot cues, on-screen captions). NOTE: this currently returns the SCRIPT only — rendered video (an actual .mp4) is pending provider keys (Creatomate / HeyGen) and is NOT produced yet; no video URL is returned.', 'address, features, highlights, length, voice'],
|
|
173
|
+
];
|
|
174
|
+
for (const [name, id, desc, fields] of SPEC_TOOL_ALIASES) {
|
|
175
|
+
server.registerTool(
|
|
176
|
+
name,
|
|
177
|
+
{
|
|
178
|
+
title: name,
|
|
179
|
+
description: desc + ' Pass an `inputs` object with: ' + fields + '. Runs in your saved voice; uses plan credits.',
|
|
180
|
+
inputSchema: { inputs: z.record(z.any()).describe('Tool inputs — ' + fields) },
|
|
181
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
182
|
+
},
|
|
183
|
+
async ({ inputs }) => callTool(id, inputs),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Spec aliases over non-/api/tool backends (async job + asset list). Same auth (CC_API_KEY), same base URL.
|
|
188
|
+
// list_assets -> GET /api/assets/list (the member's recent generated outputs from the Content Library).
|
|
189
|
+
server.registerTool('list_assets', {
|
|
190
|
+
title: 'list_assets',
|
|
191
|
+
description: 'List your recent generated outputs (the Content Library) — newest first: { id, tool, title, fav, ts, result }. Matches the 2026-06-23 connector spec.',
|
|
192
|
+
inputSchema: {},
|
|
193
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
194
|
+
}, async () => getPath('/api/assets/list'));
|
|
195
|
+
|
|
196
|
+
// job_status -> GET /api/job/<id> — poll a long-running async job (e.g. video render) until done/error.
|
|
197
|
+
server.registerTool('job_status', {
|
|
198
|
+
title: 'job_status',
|
|
199
|
+
description: 'Check the status of an async Close Cadence job (e.g. a long-running video render) by its job id. Returns { status, result, error }. Matches the 2026-06-23 connector spec.',
|
|
200
|
+
inputSchema: { job_id: z.string().describe('The job id returned when the async job was submitted (e.g. job_….).') },
|
|
201
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
202
|
+
}, async ({ job_id }) => getPath('/api/job/' + encodeURIComponent(job_id)));
|
|
203
|
+
|
|
88
204
|
await server.connect(new StdioServerTransport());
|