closecadence-mcp 0.1.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.
- package/README.md +43 -0
- package/package.json +15 -0
- package/src/index.js +88 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# closecadence-mcp
|
|
2
|
+
|
|
3
|
+
Use your **Close Cadence** real-estate tools from your own Claude. This is a tiny MCP server that lets
|
|
4
|
+
Claude (Desktop / Code) call your Close Cadence tools — listings, CMAs, social, follow-ups, flyers,
|
|
5
|
+
photo enhancement — directly, in your saved voice, on your plan's credits.
|
|
6
|
+
|
|
7
|
+
It's a thin client: it only forwards your authenticated requests to `closecadence.ai`. No tool logic or
|
|
8
|
+
data lives in this package.
|
|
9
|
+
|
|
10
|
+
## Setup (about 1 minute)
|
|
11
|
+
1. Get your key in the portal → **API key** (format `cck.cus_….…`).
|
|
12
|
+
2. Add this to your Claude config (Claude Desktop → Settings → Developer → Edit Config):
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"closecadence": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "closecadence-mcp"],
|
|
20
|
+
"env": { "CC_API_KEY": "cck.PASTE_YOUR_KEY" }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
3. Restart Claude. Now ask things like *"write a just-listed package for 412 Bayview Dr, $1.15M, 4-bed ocean view"* — Claude uses your tools, in your voice.
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
- `cc_listing`, `cc_cma`, `cc_social`, `cc_email`, `cc_followup`, `cc_market_report`, `cc_open_house`, `cc_prospecting`, `cc_listing_presentation`
|
|
29
|
+
- `cc_comps`, `cc_property` (property data) · `cc_flyer`, `cc_enhance` (media)
|
|
30
|
+
- `cc_list_tools` (what's on your plan) · `cc_run` (any tool by id)
|
|
31
|
+
|
|
32
|
+
If your membership lapses or you run out of credits, the tools say so clearly and point you to the portal.
|
|
33
|
+
|
|
34
|
+
## Develop / test
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npx @modelcontextprotocol/inspector node src/index.js # set CC_API_KEY in env first
|
|
38
|
+
```
|
|
39
|
+
Point `CC_API_BASE` at a preview deploy to test against staging.
|
|
40
|
+
|
|
41
|
+
## Publish (maintainers)
|
|
42
|
+
`npm publish --access public` (requires npm auth). After publishing, the portal's "Use in your own Claude"
|
|
43
|
+
config (`npx -y closecadence-mcp`) works as shown above.
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "closecadence-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Use your Close Cadence real-estate tools from your own Claude (MCP server).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "closecadence-mcp": "src/index.js" },
|
|
7
|
+
"files": ["src", "README.md"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"keywords": ["mcp", "claude", "real-estate", "closecadence"],
|
|
10
|
+
"license": "UNLICENSED",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
13
|
+
"zod": "^3.23.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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.ai. No business
|
|
4
|
+
// logic, prompts, or comp/CMA data live here (that's protected, server-side IP) — this just forwards the
|
|
5
|
+
// agent's key + inputs and returns the result. Auth: env CC_API_KEY (cck.cus_xxx.yyy). stdio transport.
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
const API = process.env.CC_API_BASE || 'https://closecadence.ai';
|
|
11
|
+
const KEY = process.env.CC_API_KEY || '';
|
|
12
|
+
|
|
13
|
+
const errText = (m) => ({ content: [{ type: 'text', text: m }], isError: true });
|
|
14
|
+
|
|
15
|
+
// Turn an API response into MCP content, surfacing license/credit problems clearly.
|
|
16
|
+
async function handle(r) {
|
|
17
|
+
let j = {};
|
|
18
|
+
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.ai/portal → API key).');
|
|
20
|
+
if (r.status === 402) return errText((j && j.error) || 'You’re out of credits for this month. Top up at closecadence.ai/portal → Credits (or turn on auto-refill).');
|
|
21
|
+
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
|
+
if (r.status === 503) return errText((j && j.error) || 'This tool is being set up — check back shortly.');
|
|
23
|
+
if (!r.ok || (j && j.error)) return errText((j && j.error) || ('Close Cadence error ' + r.status + '.'));
|
|
24
|
+
const out = j && j.result !== undefined ? j.result : j;
|
|
25
|
+
return { content: [{ type: 'text', text: typeof out === 'string' ? out : JSON.stringify(out, null, 2) }] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireKey() {
|
|
29
|
+
return KEY ? null : errText('No CC_API_KEY set. Add your Close Cadence key (portal → API key) to the MCP config "env", e.g. { "CC_API_KEY": "cck...." }.');
|
|
30
|
+
}
|
|
31
|
+
async function callTool(tool, inputs) {
|
|
32
|
+
const miss = requireKey(); if (miss) return miss;
|
|
33
|
+
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 || {} }) });
|
|
35
|
+
return await handle(r);
|
|
36
|
+
} catch (e) { return errText('Could not reach Close Cadence: ' + String(e && e.message || e)); }
|
|
37
|
+
}
|
|
38
|
+
async function callPath(path, body) {
|
|
39
|
+
const miss = requireKey(); if (miss) return miss;
|
|
40
|
+
try {
|
|
41
|
+
const r = await fetch(API + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': KEY }, body: JSON.stringify(body || {}) });
|
|
42
|
+
return await handle(r);
|
|
43
|
+
} catch (e) { return errText('Could not reach Close Cadence: ' + String(e && e.message || e)); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const server = new McpServer({ name: 'closecadence', version: '0.1.0' });
|
|
47
|
+
|
|
48
|
+
// Curated, named AI tools (Claude sees clear options). `inputs` is an object — the description lists the
|
|
49
|
+
// useful fields. Everything runs server-side in the agent's saved voice and uses their plan credits.
|
|
50
|
+
const NAMED = [
|
|
51
|
+
['cc_listing', 'listing-engine', 'Full listing marketing package: MLS remarks, long description, social posts, hooks.', 'address, price, beds, baths, sqft, features'],
|
|
52
|
+
['cc_cma', 'cma-narrative', 'CMA pricing narrative + objection handling from comps (pair with cc_comps for real data).', 'address, comps, suggested_price, seller_expectation, market'],
|
|
53
|
+
['cc_social', 'brand-post', 'On-brand social post in your voice (not a listing promo).', 'topic'],
|
|
54
|
+
['cc_email', 'email-writer', 'Client email — subject + body (market update, just-listed, nurture).', 'purpose, details'],
|
|
55
|
+
['cc_followup', 'followup-sequence', '5-touch text + email follow-up sequence for a lead.', 'lead_type, context'],
|
|
56
|
+
['cc_market_report', 'market-report', 'Neighborhood / market report from stats (uses only the numbers you give).', 'area, stats'],
|
|
57
|
+
['cc_open_house', 'open-house-kit', 'Open-house kit: social + email + neighbor invite + visitor follow-up.', 'address, details'],
|
|
58
|
+
['cc_prospecting', 'prospecting', 'Expired / FSBO outreach: letter + call script + texts.', 'type, context'],
|
|
59
|
+
['cc_listing_presentation', 'listing-presentation', 'Win-the-listing presentation content, slide by slide.', 'address, property, seller_goal, market, differentiators'],
|
|
60
|
+
];
|
|
61
|
+
for (const [name, id, desc, fields] of NAMED) {
|
|
62
|
+
server.registerTool(
|
|
63
|
+
name,
|
|
64
|
+
{
|
|
65
|
+
title: name,
|
|
66
|
+
description: desc + ' Pass an `inputs` object with: ' + fields + '. Runs in your saved voice; uses plan credits.',
|
|
67
|
+
inputSchema: { inputs: z.record(z.any()).describe('Tool inputs — ' + fields) },
|
|
68
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
69
|
+
},
|
|
70
|
+
async ({ inputs }) => callTool(id, inputs),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Data + media tools (live once the provider keys are configured; until then they report "being set up").
|
|
75
|
+
server.registerTool('cc_comps', { title: 'cc_comps', description: 'Pull comparable sales + an AVM for an address — the real data behind a CMA.', inputSchema: { address: z.string().describe('Full property address') } }, async ({ address }) => callPath('/api/data/comps', { address }));
|
|
76
|
+
server.registerTool('cc_property', { title: 'cc_property', description: 'Property records for an address (tax, owner, characteristics, sales history).', inputSchema: { address: z.string() } }, async ({ address }) => callPath('/api/data/property', { address }));
|
|
77
|
+
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
|
+
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
|
+
|
|
80
|
+
// Discovery + escape hatch.
|
|
81
|
+
server.registerTool('cc_list_tools', { title: 'cc_list_tools', description: 'List every Close Cadence tool available to your plan.', inputSchema: {} }, async () => {
|
|
82
|
+
const miss = requireKey(); if (miss) return miss;
|
|
83
|
+
try { return await handle(await fetch(API + '/api/tool', { headers: { 'x-api-key': KEY } })); }
|
|
84
|
+
catch (e) { return errText('Could not reach Close Cadence: ' + String(e && e.message || e)); }
|
|
85
|
+
});
|
|
86
|
+
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
|
+
|
|
88
|
+
await server.connect(new StdioServerTransport());
|