@donmorton/context-mcp-bridge 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 +29 -0
- package/dist/src/api.js +103 -0
- package/dist/src/index.js +13 -0
- package/dist/src/server.js +223 -0
- package/dist/src/tools.js +94 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Context MCP Bridge
|
|
2
|
+
|
|
3
|
+
Generic stdio MCP bridge for a hosted service. The server does not contain API secrets; it connects to a hosted API with credentials provided through environment variables.
|
|
4
|
+
|
|
5
|
+
## Required Environment
|
|
6
|
+
|
|
7
|
+
- `SERVICE_API_URL`: hosted service URL
|
|
8
|
+
- `SERVICE_API_TOKEN`: service API token
|
|
9
|
+
|
|
10
|
+
## Claude Config
|
|
11
|
+
|
|
12
|
+
Use `npx`/`pnpm dlx` from the same Node install that Claude can launch. If Node was installed with `nvm`, GUI apps may need the absolute path to `npx`.
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"context": {
|
|
18
|
+
"command": "/absolute/path/to/npx",
|
|
19
|
+
"args": ["-y", "@donmorton/context-mcp-bridge"],
|
|
20
|
+
"env": {
|
|
21
|
+
"SERVICE_API_URL": "https://YOUR-HOSTED-SERVICE-URL",
|
|
22
|
+
"SERVICE_API_TOKEN": "YOUR-SERVICE-API-TOKEN"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For a global install, use `npm install -g @donmorton/context-mcp-bridge` and set `command` to the absolute path of the installed `context-mcp-bridge` binary.
|
package/dist/src/api.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export class ServiceApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "ServiceApiError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class ServiceApiClient {
|
|
10
|
+
baseUrl;
|
|
11
|
+
apiToken;
|
|
12
|
+
fetchImpl;
|
|
13
|
+
constructor(baseUrl, apiToken, fetchImpl = fetch) {
|
|
14
|
+
this.baseUrl = baseUrl;
|
|
15
|
+
this.apiToken = apiToken;
|
|
16
|
+
this.fetchImpl = fetchImpl;
|
|
17
|
+
}
|
|
18
|
+
async getHealth() {
|
|
19
|
+
return this.request("/api/health", { auth: false });
|
|
20
|
+
}
|
|
21
|
+
async get(path, query) {
|
|
22
|
+
return this.request(withQuery(path, query));
|
|
23
|
+
}
|
|
24
|
+
async post(path, body) {
|
|
25
|
+
return this.request(path, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) })
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async patch(path, body) {
|
|
31
|
+
return this.request(path, {
|
|
32
|
+
method: "PATCH",
|
|
33
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) })
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async delete(path) {
|
|
37
|
+
return this.request(path, { method: "DELETE" });
|
|
38
|
+
}
|
|
39
|
+
async getCompany(companyKey) {
|
|
40
|
+
return this.request(`/api/companies/${encodeURIComponent(companyKey)}`);
|
|
41
|
+
}
|
|
42
|
+
async enrichCompanyContacts(companyKey) {
|
|
43
|
+
return this.post(`/api/companies/${encodeURIComponent(companyKey)}/apollo/enrich`);
|
|
44
|
+
}
|
|
45
|
+
async removeCompanyContacts(companyKey) {
|
|
46
|
+
return this.delete(`/api/companies/${encodeURIComponent(companyKey)}/contact`);
|
|
47
|
+
}
|
|
48
|
+
async request(path, options = {}) {
|
|
49
|
+
const url = new URL(path, stripTrailingSlash(this.baseUrl));
|
|
50
|
+
const { auth = true, ...requestOptions } = options;
|
|
51
|
+
const headers = new Headers(options.headers);
|
|
52
|
+
if (auth)
|
|
53
|
+
headers.set("Authorization", `Bearer ${this.apiToken}`);
|
|
54
|
+
if (options.body !== undefined && !headers.has("Content-Type"))
|
|
55
|
+
headers.set("Content-Type", "application/json");
|
|
56
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
57
|
+
...requestOptions,
|
|
58
|
+
headers
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const body = await safeReadText(response);
|
|
62
|
+
const detail = body || response.statusText || "Request failed";
|
|
63
|
+
throw new ServiceApiError(response.status, `Service API request failed with ${response.status}: ${detail}`);
|
|
64
|
+
}
|
|
65
|
+
return (await response.json());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function createServiceApiClientFromEnv(env = process.env, fetchImpl) {
|
|
69
|
+
const baseUrl = env.SERVICE_API_URL?.trim();
|
|
70
|
+
const apiToken = env.SERVICE_API_TOKEN?.trim();
|
|
71
|
+
if (!baseUrl)
|
|
72
|
+
throw new Error("SERVICE_API_URL is required");
|
|
73
|
+
if (!apiToken)
|
|
74
|
+
throw new Error("SERVICE_API_TOKEN is required");
|
|
75
|
+
return new ServiceApiClient(baseUrl, apiToken, fetchImpl);
|
|
76
|
+
}
|
|
77
|
+
async function safeReadText(response) {
|
|
78
|
+
try {
|
|
79
|
+
return await response.text();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function stripTrailingSlash(value) {
|
|
86
|
+
return value.replace(/\/+$/, "");
|
|
87
|
+
}
|
|
88
|
+
function withQuery(path, query = {}) {
|
|
89
|
+
const params = new URLSearchParams();
|
|
90
|
+
for (const [key, value] of Object.entries(query)) {
|
|
91
|
+
if (value === undefined || value === null || value === "")
|
|
92
|
+
continue;
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
for (const item of value)
|
|
95
|
+
params.append(key, String(item));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
params.set(key, String(value));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const text = params.toString();
|
|
102
|
+
return text ? `${path}?${text}` : path;
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createContextMcpServer } from "./server.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = createContextMcpServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
}
|
|
9
|
+
void main().catch((error) => {
|
|
10
|
+
const message = error instanceof Error ? error.message : "Context MCP server failed";
|
|
11
|
+
console.error(message);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createServiceApiClientFromEnv } from "./api.js";
|
|
4
|
+
import { enrichCompanyContactsSafely, refreshCompanyContacts, removeCompanyContacts, contextCapabilities } from "./tools.js";
|
|
5
|
+
export function createContextMcpServer(options = {}) {
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "context-bridge",
|
|
8
|
+
version: "0.1.0"
|
|
9
|
+
});
|
|
10
|
+
registerContextTools(server, options.clientFactory ?? createServiceApiClientFromEnv);
|
|
11
|
+
return server;
|
|
12
|
+
}
|
|
13
|
+
export function registerContextTools(server, clientFactory) {
|
|
14
|
+
server.registerTool("context_health", {
|
|
15
|
+
description: "Check whether the hosted service API is reachable.",
|
|
16
|
+
inputSchema: {}
|
|
17
|
+
}, async () => textResult(await clientFactory().getHealth()));
|
|
18
|
+
server.registerTool("context_capabilities", {
|
|
19
|
+
description: "List Context MCP capabilities and contact-enrichment cost guidance.",
|
|
20
|
+
inputSchema: {}
|
|
21
|
+
}, async () => textResult(contextCapabilities()));
|
|
22
|
+
server.registerTool("context_list_jobs", {
|
|
23
|
+
description: "List service jobs with optional filters.",
|
|
24
|
+
inputSchema: jobListSchema
|
|
25
|
+
}, async (input) => textResult(await clientFactory().get("/api/jobs", cleanQuery(input))));
|
|
26
|
+
server.registerTool("context_get_job", {
|
|
27
|
+
description: "Get one service job by id.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
id: z.string().min(1)
|
|
30
|
+
}
|
|
31
|
+
}, async ({ id }) => textResult(await clientFactory().get(`/api/jobs/${encodeURIComponent(id)}`)));
|
|
32
|
+
server.registerTool("context_list_companies", {
|
|
33
|
+
description: "List companies aggregated from service jobs.",
|
|
34
|
+
inputSchema: companyListSchema
|
|
35
|
+
}, async (input) => textResult(await clientFactory().get("/api/companies", cleanQuery(input))));
|
|
36
|
+
server.registerTool("context_get_company", {
|
|
37
|
+
description: "Get a company profile, including contact enrichment status when available.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
companyKey: z.string().min(1)
|
|
40
|
+
}
|
|
41
|
+
}, async ({ companyKey }) => textResult(await clientFactory().get(`/api/companies/${encodeURIComponent(companyKey)}`)));
|
|
42
|
+
server.registerTool("context_get_analytics", {
|
|
43
|
+
description: "Get service analytics with optional filters.",
|
|
44
|
+
inputSchema: analyticsSchema
|
|
45
|
+
}, async (input) => textResult(await clientFactory().get("/api/analytics", cleanQuery(input))));
|
|
46
|
+
server.registerTool("context_list_saved_searches", {
|
|
47
|
+
description: "List saved Google Jobs searches.",
|
|
48
|
+
inputSchema: {}
|
|
49
|
+
}, async () => textResult(await clientFactory().get("/api/searches")));
|
|
50
|
+
server.registerTool("context_list_fetch_runs", {
|
|
51
|
+
description: "List recent fetch runs.",
|
|
52
|
+
inputSchema: {}
|
|
53
|
+
}, async () => textResult(await clientFactory().get("/api/fetch-runs")));
|
|
54
|
+
server.registerTool("context_get_settings", {
|
|
55
|
+
description: "Get service settings.",
|
|
56
|
+
inputSchema: {}
|
|
57
|
+
}, async () => textResult(await clientFactory().get("/api/settings")));
|
|
58
|
+
server.registerTool("context_get_search_suggestions", {
|
|
59
|
+
description: "Get suggested service searches and Google Jobs filter guidance.",
|
|
60
|
+
inputSchema: {}
|
|
61
|
+
}, async () => textResult(await clientFactory().get("/api/search-suggestions")));
|
|
62
|
+
server.registerTool("context_trigger_fetch", {
|
|
63
|
+
description: "Trigger all enabled searches, one saved search, or a list of saved searches.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
savedSearchId: z.string().min(1).optional(),
|
|
66
|
+
savedSearchIds: z.array(z.string().min(1)).min(1).optional()
|
|
67
|
+
}
|
|
68
|
+
}, async (input) => textResult(await clientFactory().post("/api/fetch-runs", cleanBody(input))));
|
|
69
|
+
server.registerTool("context_review_job", {
|
|
70
|
+
description: "Update a job review status.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
id: z.string().min(1),
|
|
73
|
+
status: reviewStatusSchema,
|
|
74
|
+
reason: z.string().max(120).optional().nullable()
|
|
75
|
+
}
|
|
76
|
+
}, async ({ id, ...body }) => textResult(await clientFactory().patch(`/api/jobs/${encodeURIComponent(id)}/review`, cleanBody(body))));
|
|
77
|
+
server.registerTool("context_enrich_job", {
|
|
78
|
+
description: "Run job-level enrichment for source/place/pay/scoring data.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
id: z.string().min(1)
|
|
81
|
+
}
|
|
82
|
+
}, async ({ id }) => textResult(await clientFactory().post(`/api/jobs/${encodeURIComponent(id)}/enrich`)));
|
|
83
|
+
server.registerTool("context_retry_job_place", {
|
|
84
|
+
description: "Retry Google Places enrichment for a job.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
id: z.string().min(1)
|
|
87
|
+
}
|
|
88
|
+
}, async ({ id }) => textResult(await clientFactory().post(`/api/jobs/${encodeURIComponent(id)}/place/retry`)));
|
|
89
|
+
server.registerTool("context_update_job_place", {
|
|
90
|
+
description: "Select, manually set, or clear the place attached to a job.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
id: z.string().min(1),
|
|
93
|
+
action: z.enum(["select_candidate", "manual", "clear"]),
|
|
94
|
+
cid: z.string().optional().nullable(),
|
|
95
|
+
title: z.string().min(1).optional(),
|
|
96
|
+
address: z.string().min(1).optional(),
|
|
97
|
+
phoneNumber: z.string().optional().nullable(),
|
|
98
|
+
website: z.string().optional().nullable(),
|
|
99
|
+
latitude: z.number().optional().nullable(),
|
|
100
|
+
longitude: z.number().optional().nullable()
|
|
101
|
+
}
|
|
102
|
+
}, async ({ id, ...body }) => textResult(await clientFactory().patch(`/api/jobs/${encodeURIComponent(id)}/place`, cleanBody(body))));
|
|
103
|
+
server.registerTool("context_create_saved_search", {
|
|
104
|
+
description: "Create a saved Google Jobs search.",
|
|
105
|
+
inputSchema: savedSearchSchema
|
|
106
|
+
}, async (input) => textResult(await clientFactory().post("/api/searches", cleanBody(input))));
|
|
107
|
+
server.registerTool("context_update_saved_search", {
|
|
108
|
+
description: "Update a saved Google Jobs search.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
id: z.string().min(1),
|
|
111
|
+
...partialSavedSearchSchema
|
|
112
|
+
}
|
|
113
|
+
}, async ({ id, ...body }) => textResult(await clientFactory().patch(`/api/searches/${encodeURIComponent(id)}`, cleanBody(body))));
|
|
114
|
+
server.registerTool("context_bulk_update_searches", {
|
|
115
|
+
description: "Bulk update enabled saved searches.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
ids: z.array(z.string().min(1)).min(1),
|
|
118
|
+
maxPages: z.number().int().min(1).max(10).optional(),
|
|
119
|
+
fetchIntervalDays: fetchIntervalSchema.optional()
|
|
120
|
+
}
|
|
121
|
+
}, async (input) => textResult(await clientFactory().patch("/api/searches/bulk", cleanBody(input))));
|
|
122
|
+
server.registerTool("context_update_settings", {
|
|
123
|
+
description: "Update service settings.",
|
|
124
|
+
inputSchema: settingsSchema
|
|
125
|
+
}, async (input) => textResult(await clientFactory().patch("/api/settings", cleanBody(input))));
|
|
126
|
+
server.registerTool("context_enrich_company_contacts", {
|
|
127
|
+
description: "Safely enrich company contacts. Reads cached enrichment first and only performs expensive enrichment when missing or due.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
companyKey: z.string().min(1).describe("Company key from hosted service.")
|
|
130
|
+
}
|
|
131
|
+
}, async ({ companyKey }) => textResult(await enrichCompanyContactsSafely(clientFactory(), companyKey)));
|
|
132
|
+
server.registerTool("context_refresh_company_contacts", {
|
|
133
|
+
description: "Force-refresh company contact enrichment. Expensive; use only after explicit user confirmation.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
companyKey: z.string().min(1).describe("Company key from hosted service."),
|
|
136
|
+
confirmExpensive: z.literal(true).describe("Must be true after the user confirms this expensive forced refresh.")
|
|
137
|
+
}
|
|
138
|
+
}, async ({ companyKey, confirmExpensive }) => textResult(await refreshCompanyContacts(clientFactory(), { companyKey, confirmExpensive })));
|
|
139
|
+
server.registerTool("context_remove_company_contacts", {
|
|
140
|
+
description: "Remove stored contact enrichment for a company.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
companyKey: z.string().min(1).describe("Company key from hosted service.")
|
|
143
|
+
}
|
|
144
|
+
}, async ({ companyKey }) => textResult(await removeCompanyContacts(clientFactory(), companyKey)));
|
|
145
|
+
}
|
|
146
|
+
const companySizeSchema = z.enum(["small", "medium", "large", "unknown"]);
|
|
147
|
+
const fetchIntervalSchema = z.union([z.literal(1), z.literal(3), z.literal(7)]);
|
|
148
|
+
const reviewStatusSchema = z.enum(["new", "accepted", "rejected", "needs_review"]);
|
|
149
|
+
const analyticsScopeSchema = z.enum(["qualified", "all"]);
|
|
150
|
+
const payIntervalSchema = z.enum(["hour", "day", "week", "month", "year", "unknown"]);
|
|
151
|
+
const jobListSchema = {
|
|
152
|
+
status: reviewStatusSchema.optional(),
|
|
153
|
+
minScore: z.number().int().min(0).max(100).optional(),
|
|
154
|
+
maxScore: z.number().int().min(0).max(100).optional(),
|
|
155
|
+
city: z.string().optional(),
|
|
156
|
+
location: z.string().optional(),
|
|
157
|
+
company: z.string().optional(),
|
|
158
|
+
q: z.string().optional(),
|
|
159
|
+
qMode: z.enum(["contains", "not_contains"]).optional(),
|
|
160
|
+
enriched: z.enum(["true", "false"]).optional(),
|
|
161
|
+
companySizes: z.array(companySizeSchema).optional(),
|
|
162
|
+
dateFrom: z.string().optional(),
|
|
163
|
+
dateTo: z.string().optional(),
|
|
164
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
165
|
+
offset: z.number().int().min(0).optional()
|
|
166
|
+
};
|
|
167
|
+
const companyListSchema = {
|
|
168
|
+
q: z.string().optional(),
|
|
169
|
+
minScore: z.number().int().min(0).max(100).optional(),
|
|
170
|
+
companySizes: z.array(companySizeSchema).optional(),
|
|
171
|
+
dateFrom: z.string().optional(),
|
|
172
|
+
dateTo: z.string().optional(),
|
|
173
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
174
|
+
offset: z.number().int().min(0).optional()
|
|
175
|
+
};
|
|
176
|
+
const analyticsSchema = {
|
|
177
|
+
scope: analyticsScopeSchema.optional(),
|
|
178
|
+
status: reviewStatusSchema.optional(),
|
|
179
|
+
minScore: z.number().int().min(0).max(100).optional(),
|
|
180
|
+
city: z.string().optional(),
|
|
181
|
+
company: z.string().optional(),
|
|
182
|
+
companySizes: z.array(companySizeSchema).optional(),
|
|
183
|
+
dateFrom: z.string().optional(),
|
|
184
|
+
dateTo: z.string().optional(),
|
|
185
|
+
payInterval: payIntervalSchema.optional()
|
|
186
|
+
};
|
|
187
|
+
const savedSearchSchema = {
|
|
188
|
+
name: z.string().min(1),
|
|
189
|
+
query: z.string().min(1),
|
|
190
|
+
location: z.string().min(1),
|
|
191
|
+
uds: z.string().min(1).optional().nullable(),
|
|
192
|
+
gl: z.string().min(2).optional(),
|
|
193
|
+
hl: z.string().min(2).optional(),
|
|
194
|
+
googleDomain: z.string().min(1).optional(),
|
|
195
|
+
maxPages: z.number().int().min(1).max(10).optional(),
|
|
196
|
+
fetchIntervalDays: fetchIntervalSchema.optional(),
|
|
197
|
+
enabled: z.boolean().optional()
|
|
198
|
+
};
|
|
199
|
+
const partialSavedSearchSchema = Object.fromEntries(Object.entries(savedSearchSchema).map(([key, value]) => [key, value.optional()]));
|
|
200
|
+
const settingsSchema = {
|
|
201
|
+
apolloAutoEnrichEnabled: z.boolean().optional(),
|
|
202
|
+
apolloTargetSmallCompanies: z.boolean().optional(),
|
|
203
|
+
apolloTargetMediumCompanies: z.boolean().optional(),
|
|
204
|
+
apolloRefetchIntervalDays: z.number().int().min(1).max(365).optional().nullable(),
|
|
205
|
+
apolloMaxContacts: z.number().int().min(1).max(6).optional(),
|
|
206
|
+
apolloRevealEmails: z.boolean().optional()
|
|
207
|
+
};
|
|
208
|
+
function cleanQuery(input) {
|
|
209
|
+
return cleanBody(input);
|
|
210
|
+
}
|
|
211
|
+
function cleanBody(input) {
|
|
212
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
213
|
+
}
|
|
214
|
+
function textResult(value) {
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: JSON.stringify(value, null, 2)
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function contextCapabilities() {
|
|
2
|
+
return {
|
|
3
|
+
name: "Context MCP Bridge",
|
|
4
|
+
reads: [
|
|
5
|
+
"context_health",
|
|
6
|
+
"context_capabilities",
|
|
7
|
+
"context_list_jobs",
|
|
8
|
+
"context_get_job",
|
|
9
|
+
"context_list_companies",
|
|
10
|
+
"context_get_company",
|
|
11
|
+
"context_get_analytics",
|
|
12
|
+
"context_list_saved_searches",
|
|
13
|
+
"context_list_fetch_runs",
|
|
14
|
+
"context_get_settings",
|
|
15
|
+
"context_get_search_suggestions"
|
|
16
|
+
],
|
|
17
|
+
writes: [
|
|
18
|
+
"context_trigger_fetch",
|
|
19
|
+
"context_review_job",
|
|
20
|
+
"context_enrich_job",
|
|
21
|
+
"context_retry_job_place",
|
|
22
|
+
"context_update_job_place",
|
|
23
|
+
"context_create_saved_search",
|
|
24
|
+
"context_update_saved_search",
|
|
25
|
+
"context_bulk_update_searches",
|
|
26
|
+
"context_update_settings",
|
|
27
|
+
"context_enrich_company_contacts",
|
|
28
|
+
"context_refresh_company_contacts",
|
|
29
|
+
"context_remove_company_contacts"
|
|
30
|
+
],
|
|
31
|
+
guidance: [
|
|
32
|
+
"Use read tools for lead and company inspection before mutating data.",
|
|
33
|
+
"Use context_enrich_company_contacts before refresh; it checks cached enrichment and avoids expensive refreshes unless data is missing or due.",
|
|
34
|
+
"Use context_refresh_company_contacts only after the user explicitly confirms a forced refresh because contact enrichment is expensive.",
|
|
35
|
+
"Use context_remove_company_contacts only when the user asks to clear stored company contacts."
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function enrichCompanyContactsSafely(client, companyKey, now = new Date()) {
|
|
40
|
+
const profile = await client.getCompany(companyKey);
|
|
41
|
+
const decision = shouldRunSafeCompanyContactEnrichment(profile, now);
|
|
42
|
+
if (!decision.run) {
|
|
43
|
+
return {
|
|
44
|
+
action: "cached",
|
|
45
|
+
companyKey,
|
|
46
|
+
expensiveRequestMade: false,
|
|
47
|
+
reason: decision.reason,
|
|
48
|
+
apollo: profile.apollo
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
action: "enriched",
|
|
53
|
+
companyKey,
|
|
54
|
+
expensiveRequestMade: true,
|
|
55
|
+
reason: decision.reason,
|
|
56
|
+
apollo: await client.enrichCompanyContacts(companyKey)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export async function refreshCompanyContacts(client, input) {
|
|
60
|
+
if (input.confirmExpensive !== true) {
|
|
61
|
+
throw new Error("Set confirmExpensive to true only after the user confirms a forced contact enrichment refresh. Enrichment is expensive.");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
action: "refreshed",
|
|
65
|
+
companyKey: input.companyKey,
|
|
66
|
+
expensiveRequestMade: true,
|
|
67
|
+
reason: "Forced contact enrichment refresh was explicitly confirmed.",
|
|
68
|
+
apollo: await client.enrichCompanyContacts(input.companyKey)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export async function removeCompanyContacts(client, companyKey) {
|
|
72
|
+
return {
|
|
73
|
+
action: "removed",
|
|
74
|
+
companyKey,
|
|
75
|
+
expensiveRequestMade: false,
|
|
76
|
+
reason: "Stored company contact enrichment was removed.",
|
|
77
|
+
apollo: await client.removeCompanyContacts(companyKey)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function shouldRunSafeCompanyContactEnrichment(profile, now = new Date()) {
|
|
81
|
+
const apollo = profile.apollo;
|
|
82
|
+
if (!apollo || apollo.status === "NOT_RUN") {
|
|
83
|
+
return { run: true, reason: "No stored contact enrichment exists for this company." };
|
|
84
|
+
}
|
|
85
|
+
if (apollo.nextEligibleAt && Date.parse(apollo.nextEligibleAt) <= now.getTime()) {
|
|
86
|
+
return { run: true, reason: "Stored contact enrichment is due for refresh." };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
run: false,
|
|
90
|
+
reason: apollo.nextEligibleAt
|
|
91
|
+
? `Stored contact enrichment is not due until ${apollo.nextEligibleAt}. Use context_refresh_company_contacts only after explicit user confirmation.`
|
|
92
|
+
: "Stored contact enrichment already exists and automatic refresh is disabled. Use context_refresh_company_contacts only after explicit user confirmation."
|
|
93
|
+
};
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@donmorton/context-mcp-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generic stdio MCP bridge for a hosted service.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/src/index.js",
|
|
8
|
+
"types": "./dist/src/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"context-mcp-bridge": "dist/src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"prepack": "pnpm build",
|
|
22
|
+
"start": "tsx src/index.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"zod": "^3.25.42"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.15.29",
|
|
32
|
+
"tsx": "^4.19.4",
|
|
33
|
+
"typescript": "^5.8.3",
|
|
34
|
+
"vitest": "^3.2.1"
|
|
35
|
+
}
|
|
36
|
+
}
|