@mackody/quickenrich-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.
Files changed (36) hide show
  1. package/.codex-plugin/plugin.json +39 -0
  2. package/.env.example +17 -0
  3. package/.mcp.json +17 -0
  4. package/README.md +283 -0
  5. package/agents/AGENTS.md +6 -0
  6. package/agents/CLAUDE.md +6 -0
  7. package/package.json +40 -0
  8. package/scripts/agent-docs-smoke.mjs +72 -0
  9. package/scripts/agent-envelope-workflow-smoke.mjs +92 -0
  10. package/scripts/agent-surface-smoke.mjs +102 -0
  11. package/scripts/benchmark-contract-smoke.mjs +46 -0
  12. package/scripts/envelope-budget-smoke.mjs +37 -0
  13. package/scripts/killer-contract-smoke.mjs +206 -0
  14. package/scripts/lib/benchmark.mjs +92 -0
  15. package/scripts/lib/core.mjs +3303 -0
  16. package/scripts/live-benchmark.mjs +518 -0
  17. package/scripts/live-smoke.mjs +61 -0
  18. package/scripts/local-floor-benchmark.mjs +366 -0
  19. package/scripts/mcp-config-smoke.mjs +110 -0
  20. package/scripts/mcp-live-smoke.mjs +115 -0
  21. package/scripts/mock-api-smoke.mjs +112 -0
  22. package/scripts/native-capabilities-smoke.mjs +273 -0
  23. package/scripts/native-live-smoke.mjs +43 -0
  24. package/scripts/offline-contract-smoke.mjs +80 -0
  25. package/scripts/ops-contract-smoke.mjs +270 -0
  26. package/scripts/paged-finder-floor-smoke.mjs +93 -0
  27. package/scripts/perf-contract-smoke.mjs +111 -0
  28. package/scripts/quickenrich-agent-docs.mjs +109 -0
  29. package/scripts/quickenrich-mcp.mjs +201 -0
  30. package/scripts/quickenrich-token.mjs +175 -0
  31. package/scripts/rate-limit-contract-smoke.mjs +70 -0
  32. package/scripts/reverse-concurrency-floor-smoke.mjs +82 -0
  33. package/scripts/serialization-floor-smoke.mjs +42 -0
  34. package/scripts/token-bucket-contract-smoke.mjs +121 -0
  35. package/scripts/token-cli-smoke.mjs +59 -0
  36. package/skills/quickenrich/SKILL.md +20 -0
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "quickenrich-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Standalone MCP server and CLI for Quick Enrich lead enrichment APIs.",
5
+ "author": {
6
+ "name": "Cody Robertson"
7
+ },
8
+ "homepage": "https://app.quickenrich.io/docs",
9
+ "repository": "https://github.com/codyrobertson/quickenrich-mcp",
10
+ "license": "UNLICENSED",
11
+ "keywords": [
12
+ "quickenrich",
13
+ "quick-enrich",
14
+ "mcp",
15
+ "lead-enrichment",
16
+ "prospecting"
17
+ ],
18
+ "skills": "./skills/",
19
+ "interface": {
20
+ "displayName": "Quick Enrich MCP",
21
+ "shortDescription": "Use Quick Enrich APIs through a compact, batch-ready MCP server.",
22
+ "longDescription": "Quick Enrich MCP connects Codex and other MCP clients to Quick Enrich's documented APIs with secure local token storage, bearer-token auth, compact agent envelopes, exact person enrichment, reverse-email lookup, phone lookup, domain lead pulls, contact/company finder calls, and credit-aware batch lead pulling. It is a standalone repo and does not depend on PLM/Hermes runtime code.",
23
+ "developerName": "Cody Robertson",
24
+ "category": "Productivity",
25
+ "capabilities": [
26
+ "Read",
27
+ "Automation"
28
+ ],
29
+ "websiteURL": "https://app.quickenrich.io/docs",
30
+ "defaultPrompt": [
31
+ "Check Quick Enrich MCP auth status.",
32
+ "Pull founder leads for these company domains.",
33
+ "Find a verified email from a LinkedIn profile."
34
+ ],
35
+ "brandColor": "#2563EB",
36
+ "screenshots": []
37
+ },
38
+ "mcpServers": "./.mcp.json"
39
+ }
package/.env.example ADDED
@@ -0,0 +1,17 @@
1
+ QUICKENRICH_API_KEY=your-quickenrich-key
2
+ QUICKENRICH_MCP_SERVER_NAME=quick-enrich
3
+ QUICKENRICH_MCP_TOOL_MODE=agent
4
+ QUICKENRICH_MCP_RESPONSE_MODE=compact
5
+ QUICKENRICH_API_BASE_URL=https://app.quickenrich.io
6
+ QUICKENRICH_API_TIMEOUT_MS=30000
7
+ QUICKENRICH_API_RETRY_COUNT=2
8
+ QUICKENRICH_API_RETRY_BASE_MS=250
9
+ QUICKENRICH_API_USE_FETCH=0
10
+ QUICKENRICH_MCP_MAX_OUTPUT_CHARS=12000
11
+ QUICKENRICH_DOMAIN_SEARCH_REQUESTS_PER_MINUTE=300
12
+ QUICKENRICH_DOMAIN_SEARCH_BURST_SIZE=8
13
+ QUICKENRICH_EMPLOYEE_SEARCH_BURST_SIZE=16
14
+ QUICKENRICH_PHONE_SEARCH_BURST_SIZE=16
15
+ QUICKENRICH_REVERSE_EMAIL_BURST_SIZE=16
16
+ QUICKENRICH_CONTACT_FINDER_BURST_SIZE=4
17
+ QUICKENRICH_COMPANY_FINDER_BURST_SIZE=4
package/.mcp.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "quick-enrich": {
4
+ "command": "sh",
5
+ "args": [
6
+ "-lc",
7
+ "exec node \"${CLAUDE_PLUGIN_ROOT:-.}/scripts/quickenrich-mcp.mjs\""
8
+ ],
9
+ "cwd": ".",
10
+ "env": {
11
+ "QUICKENRICH_MCP_SERVER_NAME": "quick-enrich",
12
+ "QUICKENRICH_MCP_TOOL_MODE": "agent",
13
+ "QUICKENRICH_MCP_RESPONSE_MODE": "compact"
14
+ }
15
+ }
16
+ }
17
+ }
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # Quick Enrich MCP
2
+
3
+ Standalone MCP server for Quick Enrich's documented APIs. It is built for internal prospecting workflows: secure local API-key storage, exact person enrichment, reverse-email/phone lookup, domain lead pulls, contact/company finder calls, and compact batch envelopes for agent use.
4
+
5
+ Docs source: [Quick Enrich API Documentation](https://app.quickenrich.io/docs).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install
11
+ npm run test
12
+ ```
13
+
14
+ Package/agent setup after install:
15
+
16
+ ```bash
17
+ quickenrich-token set
18
+ quickenrich-token doctor
19
+ quickenrich-agent-docs install --cwd /path/to/agent-project
20
+ ```
21
+
22
+ `quickenrich-agent-docs install` appends exactly two managed Quick Enrich how-to-use lines to `CLAUDE.md` and `AGENTS.md` in the target project. It is idempotent. Remove those managed lines with:
23
+
24
+ ```bash
25
+ quickenrich-agent-docs uninstall --cwd /path/to/agent-project
26
+ ```
27
+
28
+ Set an API key with either an environment variable:
29
+
30
+ ```bash
31
+ export QUICKENRICH_API_KEY=...
32
+ ```
33
+
34
+ or the local secure config:
35
+
36
+ ```bash
37
+ node scripts/quickenrich-token.mjs set
38
+ node scripts/quickenrich-token.mjs status
39
+ ```
40
+
41
+ The token CLI stores the key at `~/.config/quickenrich-mcp/config.json` with `0600` permissions. Env vars override the config file.
42
+
43
+ ## MCP
44
+
45
+ Use the bundled `.mcp.json` or run directly:
46
+
47
+ ```bash
48
+ node scripts/quickenrich-mcp.mjs
49
+ ```
50
+
51
+ MCP server alias: `quick-enrich`
52
+
53
+ Default agent tools:
54
+
55
+ - `quickenrich_auth_status`
56
+ - `quickenrich_agent_graph`
57
+ - `quickenrich_natural_language`
58
+ - `quickenrich_pipeline`
59
+ - `quickenrich_batch_pull_leads`
60
+ - `quickenrich_batch_reverse_email`
61
+ - `quickenrich_batch_employee_search`
62
+ - `quickenrich_reverse_email`
63
+ - `quickenrich_lookup_values`
64
+
65
+ Full-mode tools (`QUICKENRICH_MCP_TOOL_MODE=full`) also expose:
66
+
67
+ - `quickenrich_pipeline`
68
+ - `quickenrich_employee_search`
69
+ - `quickenrich_phone_search`
70
+ - `quickenrich_reverse_email`
71
+ - `quickenrich_batch_reverse_email`
72
+ - `quickenrich_batch_employee_search`
73
+ - `quickenrich_domain_search`
74
+ - `quickenrich_contact_finder`
75
+ - `quickenrich_company_finder`
76
+ - `quickenrich_batch_pull_leads`
77
+ - `quickenrich_lookup_values`
78
+
79
+ Use `QUICKENRICH_MCP_TOOL_MODE=minimal` for only auth, graph, natural-language planning, and batch pull. Use `QUICKENRICH_MCP_TOOL_MODE=full` when you want every low-level endpoint and verbose schema descriptions.
80
+
81
+ Default agent mode intentionally emits slim schemas and short tool descriptions so cold agents do not spend context on prose they can recover from `quickenrich_agent_graph`. The default `tools/list` surface is budgeted under 8 KB and includes lookup discovery because finder filters need valid country, industry, employee-range, revenue-range, and service values.
82
+
83
+ Responses default to compact mode with a 12 KB output budget, 12 array items, 48 object keys, depth 4, and 240-character strings. Pass `response: { "mode": "full", "maxOutputChars": 64000 }` for rawer output, or `response: { "mode": "meta" }` for shape-only output.
84
+
85
+ MCP tool text is emitted as single-line JSON by default to reduce stdio bytes and token overhead. Set `QUICKENRICH_MCP_PRETTY_JSON=1` only when debugging by hand.
86
+
87
+ The API client uses a native keep-alive HTTP transport by default for lower batch overhead. Set `QUICKENRICH_API_USE_FETCH=1` to force the global `fetch` transport while debugging.
88
+
89
+ ## Batch Pull
90
+
91
+ Example MCP arguments:
92
+
93
+ ```json
94
+ {
95
+ "companies": [
96
+ { "company_url": "example.com", "title": "Founder" },
97
+ { "company_url": "shop.example", "title": "CEO" }
98
+ ],
99
+ "per_company_limit": 20,
100
+ "pages_per_company": 1,
101
+ "has_email": true,
102
+ "concurrency": 8,
103
+ "requests_per_minute": 300,
104
+ "burst_size": 1,
105
+ "max_credits": 100,
106
+ "export_format": "jsonl",
107
+ "enrich_contacts": false
108
+ }
109
+ ```
110
+
111
+ Native batch surfaces:
112
+
113
+ - `quickenrich_pipeline`: native company-finder to domain-search workflow with durable output and projected rows.
114
+ - `quickenrich_batch_pull_leads`: domain lead pulls plus Contact Finder and Company Finder batch modes.
115
+ - `quickenrich_batch_reverse_email`: deduped reverse-email lookup from `emails`, `input_csv`, or `input_jsonl`.
116
+ - `quickenrich_batch_employee_search`: exact-person lookup from `persons`, `input_csv`, or `input_jsonl`, with optional phone enrichment.
117
+
118
+ Batch-native controls:
119
+
120
+ - `max_credits`: stops starting new paid calls after the budget is reached. Paid lanes reserve estimated credits before starting work, so they keep safe parallelism without overspending the caller's budget.
121
+ - `resume_state`: skips previously completed or failed keys and returns `resumeState` with `completed`, `failed`, and `pending` keys for continuing large runs.
122
+ - `input_file`: local CSV/JSONL path or `file://` URL for large jobs without pasting rows into prompts.
123
+ - `output_file`: local JSONL/CSV artifact path for results.
124
+ - `resume_file`: local JSON checkpoint merged with `resume_state` and updated as work completes.
125
+ - `projection`: `people`, `companies`, `emails`, `phones`, `ids`, or `full` for token-efficient row shape.
126
+ - `cache_mode`: `off`, `read`, `write`, `readwrite`, `refresh`, or `cache_only` for local duplicate-spend avoidance.
127
+ - `credit_ledger`: appends a local JSONL credit/request ledger when enabled.
128
+ - Finder-mode resumes include a filter `fingerprint` and `nextPage`; pass the returned `resumeState` back unchanged to continue a large Contact Finder or Company Finder run.
129
+ - Reverse-email batches locally fail invalid/reserved email domains before calling Quick Enrich.
130
+ - `validate_filters`: validates lookup-backed Contact Finder and Company Finder fields before calling vendor search endpoints.
131
+ - `requests_per_minute` and `burst_size`: endpoint-specific pacing controls. Env defaults are also supported, e.g. `QUICKENRICH_EMPLOYEE_SEARCH_REQUESTS_PER_MINUTE`.
132
+ - `export_format`: `jsonl` or `csv`; returned inline as `export.text`.
133
+ - `input_csv` / `input_jsonl`: native text ingestion for domain-search, reverse-email, and exact-person batch tools.
134
+ - `response:{mode:"meta"}` keeps scalar metrics and collection lengths while omitting records for low-token planning passes.
135
+ - `max_results`: target result count for Contact Finder and Company Finder pagination.
136
+ - Finder auto-pagination runs page 1 first to discover `last_page`, then fans out remaining safe pages up to `concurrency`. Credit-limited Company Finder fanout is capped by the remaining reserved budget.
137
+
138
+ Typed finder builders:
139
+
140
+ ```json
141
+ {
142
+ "mode": "contact_finder",
143
+ "filters": {
144
+ "company_name": { "include": ["Acme"], "exclude": [] },
145
+ "country_code": ["US"],
146
+ "title": { "include": ["CEO"], "exclude": ["Intern"] }
147
+ },
148
+ "has_email": true,
149
+ "has_phone": true,
150
+ "max_results": 100,
151
+ "per_page": 50
152
+ }
153
+ ```
154
+
155
+ Company Finder uses company-shaped output with `domain`, `linkedin_url`, `services`, snippets, firmographics, and contact fields preserved. Supported typed company filters include `home_page_text`, `bio_li`, `services`, `industry`, `number_of_employees`, `revenue`, `country_code`, `city`, `company_name`, and `company_url`.
156
+
157
+ Lookup-backed validation covers Contact Finder `country_code`, `industry_linkedin`, `number_of_employees`, and `revenue`; Company Finder `services`, `industry`, `country_code`, `number_of_employees`, and `revenue`. Validation is opt-in because public lookup endpoints are external dependencies; successful lookup reads are cached in-process.
158
+
159
+ Agent graph:
160
+
161
+ - `quickenrich_agent_graph` returns `endpointCoverage`, `agentRouting`, `budgetMath`, `lookupHints`, `inputHints`, and `envelopePolicy` so cold agents can choose native batch tools without reading docs.
162
+ - `quickenrich_natural_language` routes CSV/JSONL reverse-email, exact-person, domain-search, finder, and resume intents to the native batch tools. It returns `requires` for missing payloads and refuses `execute=true` when required fields are absent.
163
+
164
+ Credit/rate notes from the current docs:
165
+
166
+ - Search, phone, reverse-email: 1000 requests/minute.
167
+ - Domain search: 300 requests/minute, up to 20 contacts/page.
168
+ - Contact finder and company finder: 120 requests/minute.
169
+ - Contact finder is discovery-only and does not return email/phone values; use `enrich_contacts: true` only when you intentionally want follow-up employee/phone lookups.
170
+ - For domain search, use `has_email: true` when you only want contacts with an email signal. This is useful before paid enrichment and keeps agent envelopes cleaner.
171
+
172
+ Domain batch pulls use a shared token-bucket limiter per MCP client plus a rolling 60-second cap. The default is Quick Enrich's published `requests_per_minute=300` with `burst_size=8`, so small deduped batches start immediately while sustained traffic still refills at one token every 200 ms without exceeding the rolling minute ceiling. Set `burst_size:1` for strict one-at-a-time pacing, or set a higher lane only with provider approval:
173
+
174
+ ```bash
175
+ QUICKENRICH_DOMAIN_SEARCH_REQUESTS_PER_MINUTE=300 \
176
+ QUICKENRICH_DOMAIN_SEARCH_BURST_SIZE=8 \
177
+ node scripts/quickenrich-mcp.mjs
178
+ ```
179
+
180
+ The batch result includes `throttle` metadata showing the active ceiling, burst, total acquired slots, and sleep time. Repeated calls through the same MCP process share buckets, so follow-up batches do not reset the burst. If a final 429 includes `Retry-After`, the limiter cools down before later starts.
181
+
182
+ Use `throttle_scope:"shared"` to coordinate the rolling request ceiling across multiple local MCP processes through a small local file lock. This does not bypass published Quick Enrich limits; it keeps separate agents inside the same ceiling.
183
+
184
+ Other batch lanes also use small compliant default bursts: employee search, phone search, and reverse email default to `burst_size=16`; Contact Finder and Company Finder default to `burst_size=4`.
185
+
186
+ ## Smoke Tests
187
+
188
+ ```bash
189
+ npm run test
190
+ ```
191
+
192
+ The default suite includes:
193
+
194
+ - agent docs install/uninstall smoke
195
+ - token CLI config and redaction smoke
196
+ - offline contract smoke
197
+ - mocked Quick Enrich API smoke
198
+ - duplicate-request coalescing and concurrency smoke
199
+ - native batch capabilities smoke
200
+ - resumability and finder-validation ops smoke
201
+ - request pacing/rate-limit contract smoke
202
+ - shared token-bucket burst contract smoke
203
+ - reverse-email high-concurrency floor smoke
204
+ - paged finder floor smoke
205
+ - serialization floor smoke
206
+ - local direct-vs-MCP floor benchmark
207
+ - benchmark fixture/comparison contract smoke
208
+ - default agent-surface smoke
209
+ - compact envelope budget smoke
210
+ - cold-agent envelope/workflow smoke
211
+ - MCP stdio config smoke
212
+ - live Quick Enrich auth smoke when a key is configured
213
+ - live native batch no-credit smoke when a key is configured
214
+ - live MCP stdio auth smoke when a key is configured
215
+
216
+ The live auth probes use random reverse-email no-match calls and fail if Quick Enrich reports `credits_used > 0`.
217
+
218
+ Observed vendor diagnostic: `GET /api/lookups/country-codes` returned HTTP 500 during live testing on 2026-07-08. The live smoke records this as a non-blocking diagnostic because the authenticated no-match probe is the real auth gate.
219
+
220
+ ## Live Benchmark
221
+
222
+ ```bash
223
+ npm run bench:live
224
+ npm run bench:floor
225
+ npm run bench:credit
226
+ ```
227
+
228
+ The default benchmark uses random no-match company domains with a title filter and fails closed if Quick Enrich returns records or reports any credit usage. It compares:
229
+
230
+ - `raw_sequential`: direct Quick Enrich API calls for every input row, including duplicates.
231
+ - `raw_concurrent_deduped`: direct Quick Enrich API calls after caller-side dedupe and concurrency.
232
+ - `direct_core_batch`: the local batch tool path without stdio transport.
233
+ - `mcp_batch_stdio`: the real MCP stdio tool path.
234
+
235
+ Default shape: 2 iterations, 12 input companies per iteration, 6 unique domains, duplicate factor 2, concurrency 8, `per_page=5`. Override with `QUICKENRICH_BENCH_ITERATIONS`, `QUICKENRICH_BENCH_UNIQUE`, `QUICKENRICH_BENCH_DUPLICATE_FACTOR`, `QUICKENRICH_BENCH_CONCURRENCY`, and `QUICKENRICH_BENCH_PER_PAGE`.
236
+
237
+ Set `QUICKENRICH_BENCH_META_LANE=1` to add an extra `mcp_batch_stdio_meta` diagnostic lane. It is opt-in because shared token buckets make back-to-back lanes intentionally affect each other.
238
+
239
+ `npm run bench:credit` sets `QUICKENRICH_BENCH_SPEND_CREDITS=1`, uses real domains, and requires at least one credit to be spent unless `QUICKENRICH_BENCH_REQUIRE_CREDITS=0` is set. Override paid-run domains with `QUICKENRICH_BENCH_DOMAINS="example.com,sample.com"`.
240
+
241
+ The fair production claim is that MCP batch should beat naive raw sequential API usage by coalescing duplicate work and running the remaining requests concurrently. An already-optimized raw client that dedupes and parallelizes requests can be as fast or faster because it has no JSON-RPC stdio envelope.
242
+
243
+ `npm run bench:floor` uses a local mock API and spends no credits. It measures implementation overhead without vendor latency by default; set `QUICKENRICH_FLOOR_MOCK_LATENCY_MS` to model response latency. Latest local floor run on 2026-07-09:
244
+
245
+ - `reverse_csv_jsonl`: direct core 8.71 ms, MCP stdio 7.43 ms, 64 requests, 64 found, 11.1 KB JSONL export.
246
+ - `exact_csv_phone_csv_export`: direct core 6.99 ms, MCP stdio 6.51 ms, 64 requests, 32 found, 4.6 KB CSV export.
247
+ - `contact_finder_paging_jsonl`: direct core 2.30 ms, MCP stdio 1.43 ms, 4 paged requests, 20 leads, 4.5 KB JSONL export.
248
+ - `company_finder_paging_csv`: direct core 1.20 ms, MCP stdio 1.33 ms, 4 paged requests, 20 companies, 2.7 KB CSV export.
249
+ - Guardrail: local MCP stdio must stay within 2.5x of direct core for every floor lane.
250
+
251
+ Latest default token-bucket no-credit local run on 2026-07-09 against `https://app.quickenrich.io`:
252
+
253
+ - Shape: 2 iterations, 12 input companies, 6 unique no-match domains, duplicate factor 2, default `300/min` with burst `8`.
254
+ - `raw_sequential`: 1602.61 ms average, 12 requests/iteration, 0 credits.
255
+ - `raw_concurrent_deduped`: 194.37 ms average, 6 requests/iteration, 0 credits.
256
+ - `direct_core_batch`: 213.22 ms average, 6 requests/iteration, 0 credits.
257
+ - `mcp_batch_stdio`: 241.81 ms average, 6 requests/iteration, 0 credits.
258
+ - Result: MCP stdio batch was 6.63x faster than naive raw sequential API usage and cut request volume 2x.
259
+
260
+ Latest default token-bucket paid local run on 2026-07-09:
261
+
262
+ - Shape: 1 iteration, `danosseasoning.com` and `sagegoddess.com`, duplicate factor 2, default `300/min` with burst `8`.
263
+ - `raw_sequential`: 666.60 ms, 4 requests, 4 credits, 4 leads.
264
+ - `raw_concurrent_deduped`: 237.11 ms, 2 requests, 2 credits, 2 leads.
265
+ - `direct_core_batch`: 266.64 ms, 2 requests, 2 credits, 2 leads.
266
+ - `mcp_batch_stdio`: 318.64 ms, 2 requests, 2 credits, 2 leads.
267
+ - Result: MCP stdio batch was 2.09x faster than naive raw sequential API usage, cut request volume 2x, and saved 50% credits on duplicate inputs.
268
+
269
+ ## NPM Publish Prep
270
+
271
+ Dry-run the package contents before publishing:
272
+
273
+ ```bash
274
+ npm run check
275
+ npm run smoke
276
+ npm run pack:dry-run
277
+ ```
278
+
279
+ Actual publish is intentionally manual:
280
+
281
+ ```bash
282
+ npm publish --access public
283
+ ```
@@ -0,0 +1,6 @@
1
+ # Quick Enrich MCP Agent Notes
2
+
3
+ - Quick Enrich MCP: call `quickenrich_agent_graph` first, then prefer `quickenrich_pipeline` or batch tools with `input_file`, `output_file`, `resume_file`, `projection`, and `max_credits`.
4
+ - Quick Enrich MCP auth: run `quickenrich-token set` once and `quickenrich-token doctor` before live runs; never paste API keys into prompts.
5
+
6
+ For large CSV/JSONL work, do not paste rows into the prompt. Pass `input_file`, write records with `output_file`, checkpoint with `resume_file`, and keep live calls inside published Quick Enrich limits.
@@ -0,0 +1,6 @@
1
+ # Quick Enrich MCP Agent Notes
2
+
3
+ - Quick Enrich MCP: call `quickenrich_agent_graph` first, then prefer `quickenrich_pipeline` or batch tools with `input_file`, `output_file`, `resume_file`, `projection`, and `max_credits`.
4
+ - Quick Enrich MCP auth: run `quickenrich-token set` once and `quickenrich-token doctor` before live runs; never paste API keys into prompts.
5
+
6
+ Use `response:{mode:"meta"}` for planning, `projection:"emails"` or `projection:"people"` for compact row handoff, and `throttle_scope:"shared"` when multiple local MCP processes may run at once.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@mackody/quickenrich-mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Standalone MCP server and CLI for Quick Enrich lead enrichment APIs.",
6
+ "bin": {
7
+ "quickenrich-agent-docs": "scripts/quickenrich-agent-docs.mjs",
8
+ "quickenrich-mcp": "scripts/quickenrich-mcp.mjs",
9
+ "quickenrich-token": "scripts/quickenrich-token.mjs"
10
+ },
11
+ "files": [
12
+ ".codex-plugin/",
13
+ ".env.example",
14
+ ".mcp.json",
15
+ "agents/",
16
+ "scripts/",
17
+ "skills/",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "agent-docs:install": "node scripts/quickenrich-agent-docs.mjs install",
22
+ "agent-docs:uninstall": "node scripts/quickenrich-agent-docs.mjs uninstall",
23
+ "check": "node --check scripts/quickenrich-agent-docs.mjs && node --check scripts/quickenrich-mcp.mjs && node --check scripts/quickenrich-token.mjs && node --check scripts/lib/core.mjs && node --check scripts/lib/benchmark.mjs && node --check scripts/agent-docs-smoke.mjs && node --check scripts/token-cli-smoke.mjs && node --check scripts/offline-contract-smoke.mjs && node --check scripts/mock-api-smoke.mjs && node --check scripts/perf-contract-smoke.mjs && node --check scripts/rate-limit-contract-smoke.mjs && node --check scripts/token-bucket-contract-smoke.mjs && node --check scripts/reverse-concurrency-floor-smoke.mjs && node --check scripts/paged-finder-floor-smoke.mjs && node --check scripts/serialization-floor-smoke.mjs && node --check scripts/local-floor-benchmark.mjs && node --check scripts/native-capabilities-smoke.mjs && node --check scripts/ops-contract-smoke.mjs && node --check scripts/benchmark-contract-smoke.mjs && node --check scripts/agent-surface-smoke.mjs && node --check scripts/envelope-budget-smoke.mjs && node --check scripts/agent-envelope-workflow-smoke.mjs && node --check scripts/killer-contract-smoke.mjs && node --check scripts/live-smoke.mjs && node --check scripts/native-live-smoke.mjs && node --check scripts/mcp-live-smoke.mjs && node --check scripts/mcp-config-smoke.mjs && node --check scripts/live-benchmark.mjs",
24
+ "smoke": "node scripts/agent-docs-smoke.mjs && node scripts/token-cli-smoke.mjs && node scripts/offline-contract-smoke.mjs && node scripts/mock-api-smoke.mjs && node scripts/perf-contract-smoke.mjs && node scripts/rate-limit-contract-smoke.mjs && node scripts/token-bucket-contract-smoke.mjs && node scripts/reverse-concurrency-floor-smoke.mjs && node scripts/paged-finder-floor-smoke.mjs && node scripts/serialization-floor-smoke.mjs && node scripts/local-floor-benchmark.mjs && node scripts/native-capabilities-smoke.mjs && node scripts/ops-contract-smoke.mjs && node scripts/benchmark-contract-smoke.mjs && node scripts/agent-surface-smoke.mjs && node scripts/envelope-budget-smoke.mjs && node scripts/agent-envelope-workflow-smoke.mjs && node scripts/killer-contract-smoke.mjs && node scripts/mcp-config-smoke.mjs",
25
+ "smoke:live": "node scripts/live-smoke.mjs && node scripts/native-live-smoke.mjs && node scripts/mcp-live-smoke.mjs",
26
+ "bench:live": "node scripts/live-benchmark.mjs",
27
+ "bench:floor": "node scripts/local-floor-benchmark.mjs",
28
+ "bench:credit": "QUICKENRICH_BENCH_SPEND_CREDITS=1 node scripts/live-benchmark.mjs",
29
+ "pack:dry-run": "npm pack --dry-run",
30
+ "prepublishOnly": "npm run check && npm run smoke",
31
+ "test": "npm run check && npm run smoke && npm run smoke:live"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "license": "UNLICENSED"
40
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ function assert(condition, message) {
9
+ if (!condition) throw new Error(message);
10
+ }
11
+
12
+ function run(args, cwd) {
13
+ const result = spawnSync(process.execPath, ["scripts/quickenrich-agent-docs.mjs", ...args], {
14
+ cwd,
15
+ encoding: "utf8",
16
+ });
17
+ if (result.status !== 0) throw new Error(`Command failed: ${args.join(" ")}\n${result.stderr || result.stdout}`);
18
+ return JSON.parse(result.stdout);
19
+ }
20
+
21
+ function managedLines(text) {
22
+ return text.split(/\r?\n/).filter((line) => line.includes("<!-- quickenrich-mcp:line"));
23
+ }
24
+
25
+ const repoRoot = new URL("..", import.meta.url).pathname;
26
+ const tempDir = mkdtempSync(join(tmpdir(), "qe-agent-docs-"));
27
+
28
+ try {
29
+ const claudePath = join(tempDir, "CLAUDE.md");
30
+ const agentsPath = join(tempDir, "AGENTS.md");
31
+ writeFileSync(claudePath, "# Existing Claude\n\nKeep this line.\n");
32
+ writeFileSync(agentsPath, "# Existing Agents\n\nKeep this line.\n");
33
+
34
+ const installed = run(["install", "--cwd", tempDir, "--json"], repoRoot);
35
+ assert(installed.ok === true, "install should report ok=true.");
36
+ assert(installed.action === "install", "install action should be reported.");
37
+ assert(installed.files.length === 2, "install should touch CLAUDE.md and AGENTS.md.");
38
+
39
+ const claudeInstalled = readFileSync(claudePath, "utf8");
40
+ const agentsInstalled = readFileSync(agentsPath, "utf8");
41
+ assert(claudeInstalled.includes("Keep this line."), "install should preserve existing CLAUDE.md content.");
42
+ assert(agentsInstalled.includes("Keep this line."), "install should preserve existing AGENTS.md content.");
43
+ assert(managedLines(claudeInstalled).length === 2, "install should append exactly two managed CLAUDE.md lines.");
44
+ assert(managedLines(agentsInstalled).length === 2, "install should append exactly two managed AGENTS.md lines.");
45
+ assert(claudeInstalled.includes("quickenrich_agent_graph"), "managed lines should explain agent graph usage.");
46
+ assert(agentsInstalled.includes("quickenrich-token doctor"), "managed lines should explain auth/doctor usage.");
47
+
48
+ run(["install", "--cwd", tempDir, "--json"], repoRoot);
49
+ assert(managedLines(readFileSync(claudePath, "utf8")).length === 2, "install should be idempotent for CLAUDE.md.");
50
+ assert(managedLines(readFileSync(agentsPath, "utf8")).length === 2, "install should be idempotent for AGENTS.md.");
51
+
52
+ const uninstalled = run(["uninstall", "--cwd", tempDir, "--json"], repoRoot);
53
+ assert(uninstalled.ok === true, "uninstall should report ok=true.");
54
+ assert(uninstalled.action === "uninstall", "uninstall action should be reported.");
55
+ assert(managedLines(readFileSync(claudePath, "utf8")).length === 0, "uninstall should remove managed CLAUDE.md lines.");
56
+ assert(managedLines(readFileSync(agentsPath, "utf8")).length === 0, "uninstall should remove managed AGENTS.md lines.");
57
+ assert(readFileSync(claudePath, "utf8").includes("Keep this line."), "uninstall should preserve existing CLAUDE.md content.");
58
+ assert(readFileSync(agentsPath, "utf8").includes("Keep this line."), "uninstall should preserve existing AGENTS.md content.");
59
+
60
+ run(["uninstall", "--cwd", tempDir, "--json"], repoRoot);
61
+ assert(managedLines(readFileSync(claudePath, "utf8")).length === 0, "uninstall should be idempotent for CLAUDE.md.");
62
+ assert(managedLines(readFileSync(agentsPath, "utf8")).length === 0, "uninstall should be idempotent for AGENTS.md.");
63
+
64
+ const claudeTemplate = readFileSync(join(repoRoot, "agents", "CLAUDE.md"), "utf8");
65
+ const agentsTemplate = readFileSync(join(repoRoot, "agents", "AGENTS.md"), "utf8");
66
+ assert(claudeTemplate.includes("quickenrich_pipeline"), "agent CLAUDE.md should document the pipeline tool.");
67
+ assert(agentsTemplate.includes("input_file"), "agent AGENTS.md should document file-based bulk inputs.");
68
+
69
+ console.log(JSON.stringify({ ok: true }, null, 2));
70
+ } finally {
71
+ rmSync(tempDir, { recursive: true, force: true });
72
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ TOOL_DEFINITIONS,
5
+ agentGraph,
6
+ callQuickEnrichTool,
7
+ compactToolPayload,
8
+ planNaturalLanguage,
9
+ selectTools,
10
+ toolPayloadText,
11
+ } from "./lib/core.mjs";
12
+
13
+ function assert(condition, message) {
14
+ if (!condition) throw new Error(message);
15
+ }
16
+
17
+ function bytes(value) {
18
+ return Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value), "utf8");
19
+ }
20
+
21
+ const agentTools = selectTools(TOOL_DEFINITIONS);
22
+ const fullTools = selectTools(TOOL_DEFINITIONS, "full");
23
+ const agentToolBytes = bytes({ tools: agentTools });
24
+ const fullToolBytes = bytes({ tools: fullTools });
25
+
26
+ assert(agentTools.length <= 9, `Default agent surface should stay <=9 tools, got ${agentTools.length}.`);
27
+ assert(agentToolBytes <= 8_000, `Default tools/list surface is too large: ${agentToolBytes} bytes.`);
28
+ assert(fullToolBytes > agentToolBytes, "Full tool surface should remain more verbose than agent surface.");
29
+ assert(agentTools.some((tool) => tool.name === "quickenrich_lookup_values"), "Agent surface should expose lookup discovery.");
30
+ assert(agentTools.every((tool) => !JSON.stringify(tool).includes("description\":\"CSV text")), "Agent surface should strip verbose nested schema descriptions.");
31
+
32
+ const graph = agentGraph();
33
+ const graphCompact = compactToolPayload(graph);
34
+ const graphMeta = compactToolPayload(graph, { mode: "meta" });
35
+ const graphFull = compactToolPayload(graph, { mode: "full", maxOutputChars: 100_000 });
36
+ const graphCompactText = toolPayloadText(graphCompact);
37
+ const graphMetaText = toolPayloadText(graphMeta);
38
+ const graphFullText = toolPayloadText(graphFull);
39
+
40
+ assert(bytes(graphCompactText) <= 8_000, `Agent graph compact envelope is too large: ${bytes(graphCompactText)} bytes.`);
41
+ assert(bytes(graphMetaText) <= 1_600, `Agent graph meta envelope is too large: ${bytes(graphMetaText)} bytes.`);
42
+ assert(graphCompact._meta.responseMode === "compact", "Agent graph compact response mode should be compact.");
43
+ assert(graphMeta._meta.responseMode === "meta", "Agent graph meta response mode should be meta.");
44
+ assert(graphFull._meta.responseMode === "full", "Agent graph full response mode should be full.");
45
+ assert(graph.lookupHints?.tool === "quickenrich_lookup_values", "Agent graph should point filter discovery to lookup_values.");
46
+ assert(graph.budgetMath?.exact_email_phone?.includes("phone"), "Agent graph should explain email+phone budget math.");
47
+
48
+ const reverseCsv = planNaturalLanguage("Reverse lookup CSV emails: email\nada@example.com\ngrace@example.com");
49
+ assert(reverseCsv.tool === "quickenrich_batch_reverse_email", `CSV email route chose ${reverseCsv.tool}.`);
50
+ assert(reverseCsv.arguments.emails.length === 2, "CSV email route should extract embedded emails.");
51
+
52
+ const reverseCsvPlaceholder = planNaturalLanguage("Reverse lookup a CSV of emails");
53
+ assert(reverseCsvPlaceholder.tool === "quickenrich_batch_reverse_email", `CSV placeholder route chose ${reverseCsvPlaceholder.tool}.`);
54
+ assert(reverseCsvPlaceholder.requires.includes("input_csv"), "CSV placeholder route should require input_csv.");
55
+
56
+ const exactJsonl = planNaturalLanguage('Batch exact employee search from JSONL: {"company_url":"example.com","first_name":"Ada","last_name":"Lovelace"}');
57
+ assert(exactJsonl.tool === "quickenrich_batch_employee_search", `Exact JSONL route chose ${exactJsonl.tool}.`);
58
+ assert(exactJsonl.arguments.response?.mode === "meta", "Exact JSONL route should default to meta planning output.");
59
+ assert(exactJsonl.requires.includes("input_jsonl"), "Exact JSONL route should require input_jsonl payload.");
60
+
61
+ const domainCsv = planNaturalLanguage("Use CSV domain_search for company_url,title\nexample.com,Founder\nsample.io,CEO");
62
+ assert(domainCsv.tool === "quickenrich_batch_pull_leads", `Domain CSV route chose ${domainCsv.tool}.`);
63
+ assert(domainCsv.arguments.mode === "domain_search", `Domain CSV route mode=${domainCsv.arguments.mode}.`);
64
+ assert(!domainCsv.arguments.companies, "Domain CSV route should avoid expanding file-shaped input into object arrays.");
65
+ assert(domainCsv.requires.includes("input_csv"), "Domain CSV route should require input_csv payload.");
66
+
67
+ const creditPlan = planNaturalLanguage("pull founder leads from 50 company domains with max 100 credits");
68
+ assert(creditPlan.tool === "quickenrich_batch_pull_leads", `Credit lead route chose ${creditPlan.tool}.`);
69
+ assert(creditPlan.arguments.max_credits === 100, `Expected max_credits=100, got ${creditPlan.arguments.max_credits}.`);
70
+ assert(creditPlan.requires.includes("companies or input_csv/input_jsonl"), "Credit lead route should ask for company input when no domains are present.");
71
+
72
+ const resumePlan = planNaturalLanguage("continue a paged company finder run");
73
+ assert(resumePlan.tool === "quickenrich_batch_pull_leads", `Resume route chose ${resumePlan.tool}.`);
74
+ assert(resumePlan.arguments.mode === "company_finder", `Resume route mode=${resumePlan.arguments.mode}.`);
75
+ assert(resumePlan.requires.includes("resume_state"), "Resume route should require resume_state.");
76
+
77
+ const blockedExecute = await callQuickEnrichTool("quickenrich_natural_language", {
78
+ request: "reverse lookup a CSV of emails",
79
+ execute: true,
80
+ response: { mode: "full" },
81
+ });
82
+ assert(blockedExecute.ok === false, "Natural-language execute should not run incomplete plans.");
83
+ assert(blockedExecute.error.includes("input_csv"), "Blocked execute should name the missing input_csv requirement.");
84
+
85
+ console.log(JSON.stringify({
86
+ ok: true,
87
+ agentTools: agentTools.length,
88
+ agentToolBytes,
89
+ graphCompactBytes: bytes(graphCompactText),
90
+ graphMetaBytes: bytes(graphMetaText),
91
+ graphFullBytes: bytes(graphFullText),
92
+ }, null, 2));