closecadence-mcp 0.1.0 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/index.js +110 -1
package/README.md CHANGED
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "closecadence-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Use your Close Cadence real-estate tools from your own Claude (MCP server).",
5
5
  "type": "module",
6
6
  "bin": { "closecadence-mcp": "src/index.js" },
package/src/index.js CHANGED
@@ -6,10 +6,44 @@
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
13
  const API = process.env.CC_API_BASE || 'https://closecadence.ai';
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.
@@ -31,7 +65,7 @@ function requireKey() {
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
 
@@ -85,4 +126,72 @@ server.registerTool('cc_list_tools', { title: 'cc_list_tools', description: 'Lis
85
126
  });
86
127
  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
128
 
129
+ // Local-only: write the live Command Center dashboard board. No key/subscription needed — the dashboard
130
+ // renders purely from this local file (the permanent asset) even if the AI subscription lapses. Stays on
131
+ // the agent's device. Mirrors the desktop host's cc_save_board (board.json in the CloseCadence data dir).
132
+ function boardPath() {
133
+ let dir;
134
+ if (process.platform === 'win32') dir = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'CloseCadence');
135
+ else if (process.platform === 'darwin') dir = join(homedir(), 'Library', 'Application Support', 'CloseCadence');
136
+ else dir = join(homedir(), '.config', 'CloseCadence');
137
+ mkdirSync(dir, { recursive: true });
138
+ return join(dir, 'board.json');
139
+ }
140
+ server.registerTool('cc_save_board', {
141
+ title: 'cc_save_board',
142
+ 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.",
143
+ inputSchema: { board: z.record(z.any()).describe('The full board: { meta, summary, transactions, tasks, email, signatures, notes, income, account, assets, roi, extra_panels }.') },
144
+ annotations: { readOnlyHint: false, openWorldHint: false },
145
+ }, async ({ board }) => {
146
+ try {
147
+ writeFileSync(boardPath(), JSON.stringify(board ?? {}));
148
+ return { content: [{ type: 'text', text: 'Board saved — it’ll refresh in the Close Cadence Command Center dashboard.' }] };
149
+ } catch (e) {
150
+ return errText('Couldn’t save the board: ' + String(e && e.message || e));
151
+ }
152
+ });
153
+
154
+ // ---- Partner-spec aliases (2026-06-23 Platform Connector Architecture spec, Morgahn Grey) ----
155
+ // Thin, spec-named aliases over the EXISTING backend — additive, no breakage to the cc_* tools above.
156
+ // Single source of truth: each entry maps a spec name -> the same backend a cc_* tool already uses, so
157
+ // the mapping stays obvious and maintainable. Tool-engine aliases go to /api/tool (same id, same auth).
158
+ const SPEC_TOOL_ALIASES = [
159
+ // spec_name backend tool id (same backend as) description / inputs
160
+ ['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'],
161
+ ['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'],
162
+ // create_custom_video returns a production-ready VIDEO SCRIPT / STORYBOARD today (listing tour script).
163
+ // Rendered-video output (an actual .mp4 URL) is PENDING provider keys (Creatomate / HeyGen) — this tool
164
+ // does NOT return a video file or fake a URL; it returns the script the renderer will use once wired.
165
+ ['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'],
166
+ ];
167
+ for (const [name, id, desc, fields] of SPEC_TOOL_ALIASES) {
168
+ server.registerTool(
169
+ name,
170
+ {
171
+ title: name,
172
+ description: desc + ' Pass an `inputs` object with: ' + fields + '. Runs in your saved voice; uses plan credits.',
173
+ inputSchema: { inputs: z.record(z.any()).describe('Tool inputs — ' + fields) },
174
+ annotations: { readOnlyHint: false, openWorldHint: true },
175
+ },
176
+ async ({ inputs }) => callTool(id, inputs),
177
+ );
178
+ }
179
+
180
+ // Spec aliases over non-/api/tool backends (async job + asset list). Same auth (CC_API_KEY), same base URL.
181
+ // list_assets -> GET /api/assets/list (the member's recent generated outputs from the Content Library).
182
+ server.registerTool('list_assets', {
183
+ title: 'list_assets',
184
+ description: 'List your recent generated outputs (the Content Library) — newest first: { id, tool, title, fav, ts, result }. Matches the 2026-06-23 connector spec.',
185
+ inputSchema: {},
186
+ annotations: { readOnlyHint: true, openWorldHint: true },
187
+ }, async () => getPath('/api/assets/list'));
188
+
189
+ // job_status -> GET /api/job/<id> — poll a long-running async job (e.g. video render) until done/error.
190
+ server.registerTool('job_status', {
191
+ title: 'job_status',
192
+ 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.',
193
+ inputSchema: { job_id: z.string().describe('The job id returned when the async job was submitted (e.g. job_….).') },
194
+ annotations: { readOnlyHint: true, openWorldHint: true },
195
+ }, async ({ job_id }) => getPath('/api/job/' + encodeURIComponent(job_id)));
196
+
88
197
  await server.connect(new StdioServerTransport());