linkedin-toolkit-mcp 2.0.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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/bridge.d.ts +95 -0
- package/dist/bridge.js +259 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cli.d.ts +83 -0
- package/dist/cli.js +898 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +149 -0
- package/dist/config.js.map +1 -0
- package/dist/contract.d.ts +6770 -0
- package/dist/contract.js +926 -0
- package/dist/contract.js.map +1 -0
- package/dist/db.d.ts +64 -0
- package/dist/db.js +542 -0
- package/dist/db.js.map +1 -0
- package/dist/fake-data.d.ts +277 -0
- package/dist/fake-data.js +789 -0
- package/dist/fake-data.js.map +1 -0
- package/dist/fake-extension.d.ts +51 -0
- package/dist/fake-extension.js +151 -0
- package/dist/fake-extension.js.map +1 -0
- package/dist/gen.d.ts +5 -0
- package/dist/gen.js +22 -0
- package/dist/gen.js.map +1 -0
- package/dist/http.d.ts +27 -0
- package/dist/http.js +329 -0
- package/dist/http.js.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.js +306 -0
- package/dist/openapi.js.map +1 -0
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +65 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +7 -0
- package/dist/resources.js +73 -0
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +38 -0
- package/dist/server.js.map +1 -0
- package/dist/toolkit.d.ts +82 -0
- package/dist/toolkit.js +171 -0
- package/dist/toolkit.js.map +1 -0
- package/dist/tools.d.ts +31 -0
- package/dist/tools.js +103 -0
- package/dist/tools.js.map +1 -0
- package/dist/webhooks.d.ts +52 -0
- package/dist/webhooks.js +112 -0
- package/dist/webhooks.js.map +1 -0
- package/openapi.json +8334 -0
- package/package.json +45 -0
- package/tools.json +1205 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared core behind every surface: MCP tools, the HTTP action API and the
|
|
3
|
+
* CLI all go through one `Toolkit` instance, so they cannot drift apart.
|
|
4
|
+
*/
|
|
5
|
+
import { BridgeServer, type BridgeOptions, type BridgeResponse } from './bridge.js';
|
|
6
|
+
import { Db } from './db.js';
|
|
7
|
+
import { Webhooks } from './webhooks.js';
|
|
8
|
+
import type { ServerConfig } from './config.js';
|
|
9
|
+
import type { ActionName, RequestOrigin } from './contract.js';
|
|
10
|
+
export type ToolkitOptions = {
|
|
11
|
+
config: ServerConfig;
|
|
12
|
+
bridge?: BridgeServer;
|
|
13
|
+
db?: Db;
|
|
14
|
+
webhooks?: Webhooks;
|
|
15
|
+
bridgeOptions?: Partial<BridgeOptions>;
|
|
16
|
+
};
|
|
17
|
+
export type SyncResult = {
|
|
18
|
+
since: number;
|
|
19
|
+
syncedAt: number;
|
|
20
|
+
counts: Record<string, number>;
|
|
21
|
+
totals: Record<string, number>;
|
|
22
|
+
};
|
|
23
|
+
/** How long an unclaimed research completion is remembered, and how many. */
|
|
24
|
+
export declare const COMPLETED_RESEARCH_TTL_MS = 3600000;
|
|
25
|
+
export declare const MAX_COMPLETED_RESEARCH = 500;
|
|
26
|
+
export type ResearchPackResult = {
|
|
27
|
+
jobId: string;
|
|
28
|
+
status: 'completed';
|
|
29
|
+
done: number;
|
|
30
|
+
total: number;
|
|
31
|
+
packs: unknown[];
|
|
32
|
+
} | {
|
|
33
|
+
jobId: string;
|
|
34
|
+
status: 'running';
|
|
35
|
+
total: number;
|
|
36
|
+
etaMs?: number;
|
|
37
|
+
note: string;
|
|
38
|
+
};
|
|
39
|
+
export declare class Toolkit {
|
|
40
|
+
readonly config: ServerConfig;
|
|
41
|
+
readonly bridge: BridgeServer;
|
|
42
|
+
readonly db: Db;
|
|
43
|
+
readonly webhooks: Webhooks;
|
|
44
|
+
private researchWaiters;
|
|
45
|
+
/** jobId -> completion time, for jobs that finished before anyone waited. */
|
|
46
|
+
private researchCompleted;
|
|
47
|
+
constructor(options: ToolkitOptions);
|
|
48
|
+
start(): Promise<void>;
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
isConnected(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Call an action on the extension and mirror whatever it returns, keeping
|
|
53
|
+
* the quota snapshot that came with it. `origin` travels with the frame so
|
|
54
|
+
* the extension can tell an agent-originated write from a human one.
|
|
55
|
+
*/
|
|
56
|
+
callFull(action: ActionName, params?: unknown, options?: {
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
origin?: RequestOrigin;
|
|
59
|
+
}): Promise<BridgeResponse>;
|
|
60
|
+
/** `callFull` for callers that only want the data. */
|
|
61
|
+
call(action: ActionName, params?: unknown, options?: {
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
origin?: RequestOrigin;
|
|
64
|
+
}): Promise<unknown>;
|
|
65
|
+
/** `sync.pull` since the last sync, applied to the local mirror. */
|
|
66
|
+
sync(since?: number, origin?: RequestOrigin): Promise<SyncResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Start a research job and wait for it. Returns the finished packs, or the
|
|
69
|
+
* job id when the job outlives `timeoutMs` so the caller can poll
|
|
70
|
+
* `research.get`.
|
|
71
|
+
*/
|
|
72
|
+
researchPack(params: unknown, options?: {
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
origin?: RequestOrigin;
|
|
75
|
+
}): Promise<ResearchPackResult>;
|
|
76
|
+
/** Resolves true when the job completes, false on timeout. */
|
|
77
|
+
waitForResearch(jobId: string, timeoutMs: number): Promise<boolean>;
|
|
78
|
+
private resolveResearch;
|
|
79
|
+
/** Forget completions nobody claimed, oldest first. */
|
|
80
|
+
private pruneCompletedResearch;
|
|
81
|
+
private removeWaiter;
|
|
82
|
+
}
|
package/dist/toolkit.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared core behind every surface: MCP tools, the HTTP action API and the
|
|
3
|
+
* CLI all go through one `Toolkit` instance, so they cannot drift apart.
|
|
4
|
+
*/
|
|
5
|
+
import { BridgeServer, BridgeError, } from './bridge.js';
|
|
6
|
+
import { Db } from './db.js';
|
|
7
|
+
import { Webhooks } from './webhooks.js';
|
|
8
|
+
/** How long an unclaimed research completion is remembered, and how many. */
|
|
9
|
+
export const COMPLETED_RESEARCH_TTL_MS = 3_600_000;
|
|
10
|
+
export const MAX_COMPLETED_RESEARCH = 500;
|
|
11
|
+
export class Toolkit {
|
|
12
|
+
config;
|
|
13
|
+
bridge;
|
|
14
|
+
db;
|
|
15
|
+
webhooks;
|
|
16
|
+
researchWaiters = new Map();
|
|
17
|
+
/** jobId -> completion time, for jobs that finished before anyone waited. */
|
|
18
|
+
researchCompleted = new Map();
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.config = options.config;
|
|
21
|
+
this.bridge =
|
|
22
|
+
options.bridge ??
|
|
23
|
+
new BridgeServer({
|
|
24
|
+
port: options.config.bridgePort,
|
|
25
|
+
token: options.config.token,
|
|
26
|
+
...options.bridgeOptions,
|
|
27
|
+
});
|
|
28
|
+
this.db = options.db ?? new Db(options.config.dbPath);
|
|
29
|
+
this.webhooks = options.webhooks ?? new Webhooks({ url: options.config.webhookUrl });
|
|
30
|
+
this.bridge.on('event', (event, payload) => {
|
|
31
|
+
this.db.upsertEvent({ event, payload, at: Date.now() });
|
|
32
|
+
this.webhooks.deliver(event, payload);
|
|
33
|
+
if (event === 'research_completed') {
|
|
34
|
+
const jobId = payload?.jobId;
|
|
35
|
+
if (typeof jobId === 'string')
|
|
36
|
+
this.resolveResearch(jobId);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async start() {
|
|
41
|
+
await this.bridge.start();
|
|
42
|
+
}
|
|
43
|
+
async stop() {
|
|
44
|
+
await this.bridge.stop();
|
|
45
|
+
this.webhooks.close();
|
|
46
|
+
await this.webhooks.drain();
|
|
47
|
+
this.db.close();
|
|
48
|
+
}
|
|
49
|
+
isConnected() {
|
|
50
|
+
return this.bridge.isConnected();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Call an action on the extension and mirror whatever it returns, keeping
|
|
54
|
+
* the quota snapshot that came with it. `origin` travels with the frame so
|
|
55
|
+
* the extension can tell an agent-originated write from a human one.
|
|
56
|
+
*/
|
|
57
|
+
async callFull(action, params = {}, options = {}) {
|
|
58
|
+
const response = await this.bridge.requestFull(action, params, options);
|
|
59
|
+
this.db.recordToolResult(action, response.data);
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
/** `callFull` for callers that only want the data. */
|
|
63
|
+
async call(action, params = {}, options = {}) {
|
|
64
|
+
return (await this.callFull(action, params, options)).data;
|
|
65
|
+
}
|
|
66
|
+
/** `sync.pull` since the last sync, applied to the local mirror. */
|
|
67
|
+
async sync(since, origin) {
|
|
68
|
+
const from = since ?? this.db.lastSyncAt;
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
const payload = (await this.bridge.request('sync.pull', { since: from }, { origin }));
|
|
71
|
+
const counts = this.db.upsertSync(payload, startedAt);
|
|
72
|
+
this.db.lastSyncAt = startedAt;
|
|
73
|
+
return {
|
|
74
|
+
since: from,
|
|
75
|
+
syncedAt: startedAt,
|
|
76
|
+
counts: counts,
|
|
77
|
+
totals: this.db.counts(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Start a research job and wait for it. Returns the finished packs, or the
|
|
82
|
+
* job id when the job outlives `timeoutMs` so the caller can poll
|
|
83
|
+
* `research.get`.
|
|
84
|
+
*/
|
|
85
|
+
async researchPack(params, options = {}) {
|
|
86
|
+
const timeoutMs = options.timeoutMs ?? this.config.researchTimeoutMs;
|
|
87
|
+
const started = (await this.call('research.pack', params, { origin: options.origin }));
|
|
88
|
+
const jobId = String(started?.jobId ?? '');
|
|
89
|
+
if (!jobId) {
|
|
90
|
+
throw new BridgeError('INTERNAL', 'The extension did not return a research job id.');
|
|
91
|
+
}
|
|
92
|
+
const finished = await this.waitForResearch(jobId, timeoutMs);
|
|
93
|
+
if (!finished) {
|
|
94
|
+
return {
|
|
95
|
+
jobId,
|
|
96
|
+
status: 'running',
|
|
97
|
+
total: Number(started?.total ?? 0),
|
|
98
|
+
etaMs: started?.etaMs,
|
|
99
|
+
note: `Research is still running after ${timeoutMs}ms. Poll linkedin_research_get with jobId "${jobId}".`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const result = (await this.call('research.get', { jobId }, { origin: options.origin }));
|
|
103
|
+
return {
|
|
104
|
+
jobId,
|
|
105
|
+
status: 'completed',
|
|
106
|
+
done: Number(result?.done ?? 0),
|
|
107
|
+
total: Number(result?.total ?? 0),
|
|
108
|
+
packs: Array.isArray(result?.packs) ? result.packs : [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Resolves true when the job completes, false on timeout. */
|
|
112
|
+
waitForResearch(jobId, timeoutMs) {
|
|
113
|
+
this.pruneCompletedResearch();
|
|
114
|
+
if (this.researchCompleted.has(jobId)) {
|
|
115
|
+
this.researchCompleted.delete(jobId);
|
|
116
|
+
return Promise.resolve(true);
|
|
117
|
+
}
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
this.removeWaiter(jobId, onDone);
|
|
121
|
+
resolve(false);
|
|
122
|
+
}, timeoutMs);
|
|
123
|
+
timer.unref?.();
|
|
124
|
+
const onDone = () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolve(true);
|
|
127
|
+
};
|
|
128
|
+
const waiters = this.researchWaiters.get(jobId) ?? [];
|
|
129
|
+
waiters.push(onDone);
|
|
130
|
+
this.researchWaiters.set(jobId, waiters);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
resolveResearch(jobId) {
|
|
134
|
+
const waiters = this.researchWaiters.get(jobId);
|
|
135
|
+
this.researchWaiters.delete(jobId);
|
|
136
|
+
if (!waiters || waiters.length === 0) {
|
|
137
|
+
// Nobody is waiting yet: remember it so a waiter arriving a moment later
|
|
138
|
+
// still sees the completion, but never let this grow without bound.
|
|
139
|
+
this.researchCompleted.set(jobId, Date.now());
|
|
140
|
+
this.pruneCompletedResearch();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const waiter of waiters)
|
|
144
|
+
waiter();
|
|
145
|
+
}
|
|
146
|
+
/** Forget completions nobody claimed, oldest first. */
|
|
147
|
+
pruneCompletedResearch() {
|
|
148
|
+
const cutoff = Date.now() - COMPLETED_RESEARCH_TTL_MS;
|
|
149
|
+
for (const [jobId, at] of this.researchCompleted) {
|
|
150
|
+
if (at < cutoff)
|
|
151
|
+
this.researchCompleted.delete(jobId);
|
|
152
|
+
}
|
|
153
|
+
while (this.researchCompleted.size > MAX_COMPLETED_RESEARCH) {
|
|
154
|
+
const oldest = this.researchCompleted.keys().next().value;
|
|
155
|
+
if (oldest === undefined)
|
|
156
|
+
break;
|
|
157
|
+
this.researchCompleted.delete(oldest);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
removeWaiter(jobId, waiter) {
|
|
161
|
+
const waiters = this.researchWaiters.get(jobId);
|
|
162
|
+
if (!waiters)
|
|
163
|
+
return;
|
|
164
|
+
const next = waiters.filter((w) => w !== waiter);
|
|
165
|
+
if (next.length === 0)
|
|
166
|
+
this.researchWaiters.delete(jobId);
|
|
167
|
+
else
|
|
168
|
+
this.researchWaiters.set(jobId, next);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=toolkit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolkit.js","sourceRoot":"","sources":["../src/toolkit.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools. Every entry of `TOOLS` is registered with its zod input schema;
|
|
3
|
+
* results come back both as JSON text and as `structuredContent`, and any
|
|
4
|
+
* bridge failure becomes an `isError` result carrying the contract error shape
|
|
5
|
+
* so an agent can read `code`, `howToFix` and `retryAfter`.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { type BridgeResponse } from './bridge.js';
|
|
10
|
+
import { type RequestOrigin, type ToolDef } from './contract.js';
|
|
11
|
+
import type { Toolkit } from './toolkit.js';
|
|
12
|
+
export declare const SERVER_NAME = "linkedin-toolkit";
|
|
13
|
+
export declare const SERVER_VERSION = "2.0.0";
|
|
14
|
+
/**
|
|
15
|
+
* A tool result: the JSON text an agent reads plus machine-readable output.
|
|
16
|
+
*
|
|
17
|
+
* `rateLimit` rides alongside the data rather than inside it, so an agent that
|
|
18
|
+
* just sent an invite can see what it has left without another status call.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toolResult(data: unknown, rateLimit?: Record<string, number>): CallToolResult;
|
|
21
|
+
/** An error result an agent can act on without parsing prose. */
|
|
22
|
+
export declare function toolError(err: unknown): CallToolResult;
|
|
23
|
+
/**
|
|
24
|
+
* Run one tool by name against the toolkit. Shared by MCP and the HTTP tool
|
|
25
|
+
* route. The three server-local tools have no quota snapshot to report; a tool
|
|
26
|
+
* that reaches the extension passes on whatever came back with the answer.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runTool(toolkit: Toolkit, tool: ToolDef, args: Record<string, unknown>, origin?: RequestOrigin): Promise<BridgeResponse>;
|
|
29
|
+
export declare function registerTools(server: McpServer, toolkit: Toolkit): void;
|
|
30
|
+
/** The MCP server used by both the stdio entry point and the HTTP transport. */
|
|
31
|
+
export declare function createMcpServer(toolkit: Toolkit): McpServer;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools. Every entry of `TOOLS` is registered with its zod input schema;
|
|
3
|
+
* results come back both as JSON text and as `structuredContent`, and any
|
|
4
|
+
* bridge failure becomes an `isError` result carrying the contract error shape
|
|
5
|
+
* so an agent can read `code`, `howToFix` and `retryAfter`.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { BridgeError } from './bridge.js';
|
|
9
|
+
import { TOOLS, toolInputSchema, } from './contract.js';
|
|
10
|
+
import { registerResources } from './resources.js';
|
|
11
|
+
import { registerPrompts } from './prompts.js';
|
|
12
|
+
export const SERVER_NAME = 'linkedin-toolkit';
|
|
13
|
+
export const SERVER_VERSION = '2.0.0';
|
|
14
|
+
/**
|
|
15
|
+
* A tool result: the JSON text an agent reads plus machine-readable output.
|
|
16
|
+
*
|
|
17
|
+
* `rateLimit` rides alongside the data rather than inside it, so an agent that
|
|
18
|
+
* just sent an invite can see what it has left without another status call.
|
|
19
|
+
*/
|
|
20
|
+
export function toolResult(data, rateLimit) {
|
|
21
|
+
const structured = data !== null && typeof data === 'object' && !Array.isArray(data)
|
|
22
|
+
? data
|
|
23
|
+
: { result: data };
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: 'text', text: JSON.stringify(data ?? null) }],
|
|
26
|
+
structuredContent: rateLimit ? { ...structured, rateLimit } : structured,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** An error result an agent can act on without parsing prose. */
|
|
30
|
+
export function toolError(err) {
|
|
31
|
+
const error = err instanceof BridgeError
|
|
32
|
+
? err.toJSON()
|
|
33
|
+
: { code: 'INTERNAL', message: err instanceof Error ? err.message : String(err) };
|
|
34
|
+
return {
|
|
35
|
+
isError: true,
|
|
36
|
+
content: [{ type: 'text', text: JSON.stringify({ error }) }],
|
|
37
|
+
structuredContent: { error },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run one tool by name against the toolkit. Shared by MCP and the HTTP tool
|
|
42
|
+
* route. The three server-local tools have no quota snapshot to report; a tool
|
|
43
|
+
* that reaches the extension passes on whatever came back with the answer.
|
|
44
|
+
*/
|
|
45
|
+
export async function runTool(toolkit, tool, args, origin = 'mcp') {
|
|
46
|
+
switch (tool.name) {
|
|
47
|
+
case 'linkedin_query_sql': {
|
|
48
|
+
const sql = String(args.sql ?? '');
|
|
49
|
+
const params = (args.params ?? []);
|
|
50
|
+
return { data: toolkit.db.query(sql, params) };
|
|
51
|
+
}
|
|
52
|
+
case 'linkedin_sync': {
|
|
53
|
+
const since = typeof args.since === 'number' ? args.since : undefined;
|
|
54
|
+
return { data: await toolkit.sync(since, origin) };
|
|
55
|
+
}
|
|
56
|
+
case 'linkedin_endpoints_check':
|
|
57
|
+
// The whole point of this tool is the verification pass, so it is not
|
|
58
|
+
// left to the caller to remember the flag.
|
|
59
|
+
return await toolkit.callFull('status.get', { ...args, verify: true }, { origin });
|
|
60
|
+
case 'linkedin_research_pack':
|
|
61
|
+
return { data: await toolkit.researchPack(args, { origin }) };
|
|
62
|
+
default:
|
|
63
|
+
return await toolkit.callFull(tool.action, args, { origin });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function registerTools(server, toolkit) {
|
|
67
|
+
for (const tool of TOOLS) {
|
|
68
|
+
server.registerTool(tool.name, {
|
|
69
|
+
description: tool.description,
|
|
70
|
+
inputSchema: toolInputSchema(tool).shape,
|
|
71
|
+
annotations: {
|
|
72
|
+
readOnlyHint: !tool.write,
|
|
73
|
+
destructiveHint: false,
|
|
74
|
+
openWorldHint: tool.action !== null,
|
|
75
|
+
},
|
|
76
|
+
}, async (args) => {
|
|
77
|
+
try {
|
|
78
|
+
const { data, rateLimit } = await runTool(toolkit, tool, args ?? {}, 'mcp');
|
|
79
|
+
return toolResult(data, rateLimit);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return toolError(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The MCP server used by both the stdio entry point and the HTTP transport. */
|
|
88
|
+
export function createMcpServer(toolkit) {
|
|
89
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
90
|
+
instructions: 'LinkedIn Toolkit drives the user\'s own logged-in Chrome through a local extension. ' +
|
|
91
|
+
'Call linkedin_get_status first. Reads (search, profile, company, engagers, inbox) are safe. ' +
|
|
92
|
+
'Every write is rate-capped by the extension; invites, messages, InMails and comments also ' +
|
|
93
|
+
'queue for human approval instead of sending in the default Copilot mode, while views, ' +
|
|
94
|
+
'follows and likes are metered against the visit bucket and go out directly. Approving is ' +
|
|
95
|
+
'the user\'s job in the popup, not yours. Pass dry_run to preview a write. Use linkedin_sync ' +
|
|
96
|
+
'then linkedin_query_sql to work over past captures instead of re-scraping.',
|
|
97
|
+
});
|
|
98
|
+
registerTools(server, toolkit);
|
|
99
|
+
registerResources(server, toolkit);
|
|
100
|
+
registerPrompts(server, toolkit);
|
|
101
|
+
return server;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound webhooks: every bridge event is POSTed to the configured URL as
|
|
3
|
+
* `{ event, payload, at }`, retried at 1 s, 5 s and 25 s.
|
|
4
|
+
*
|
|
5
|
+
* Delivery is fire-and-forget. A webhook failure must never turn into a bridge
|
|
6
|
+
* failure, so nothing here is ever thrown back at the caller.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RETRY_DELAYS_MS: number[];
|
|
9
|
+
export type WebhookDelivery = {
|
|
10
|
+
event: string;
|
|
11
|
+
payload: unknown;
|
|
12
|
+
at: number;
|
|
13
|
+
};
|
|
14
|
+
export type WebhookOptions = {
|
|
15
|
+
url?: string;
|
|
16
|
+
retryDelaysMs?: number[];
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
onError?: (err: unknown, attempt: number) => void;
|
|
19
|
+
sleep?: (ms: number) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export declare class Webhooks {
|
|
22
|
+
private url?;
|
|
23
|
+
private readonly retryDelaysMs;
|
|
24
|
+
private readonly fetchImpl;
|
|
25
|
+
private readonly onError?;
|
|
26
|
+
private readonly sleep;
|
|
27
|
+
/** Resolves when every in-flight delivery has settled (tests await this). */
|
|
28
|
+
private inflight;
|
|
29
|
+
private closed;
|
|
30
|
+
/** Resolvers that cut a retry sleep short when the server shuts down. */
|
|
31
|
+
private sleepAborts;
|
|
32
|
+
constructor(options?: WebhookOptions);
|
|
33
|
+
setUrl(url: string | undefined): void;
|
|
34
|
+
/**
|
|
35
|
+
* Abandon pending retries and wake anything sleeping between them. Shutdown
|
|
36
|
+
* must not wait 31 s for a dead receiver, and a webhook is fire-and-forget:
|
|
37
|
+
* an undelivered event is not an error.
|
|
38
|
+
*/
|
|
39
|
+
close(): void;
|
|
40
|
+
get configured(): boolean;
|
|
41
|
+
/** Queue a delivery. Returns immediately; never rejects. */
|
|
42
|
+
deliver(event: string, payload: unknown, at?: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* Wait for in-flight deliveries. Once closed there is nothing worth waiting
|
|
45
|
+
* for: retries have been abandoned and any request still on the wire is
|
|
46
|
+
* fire-and-forget, so shutdown returns immediately.
|
|
47
|
+
*/
|
|
48
|
+
drain(): Promise<void>;
|
|
49
|
+
/** A retry delay that `close()` can cut short. */
|
|
50
|
+
private waitBeforeRetry;
|
|
51
|
+
private send;
|
|
52
|
+
}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound webhooks: every bridge event is POSTed to the configured URL as
|
|
3
|
+
* `{ event, payload, at }`, retried at 1 s, 5 s and 25 s.
|
|
4
|
+
*
|
|
5
|
+
* Delivery is fire-and-forget. A webhook failure must never turn into a bridge
|
|
6
|
+
* failure, so nothing here is ever thrown back at the caller.
|
|
7
|
+
*/
|
|
8
|
+
export const RETRY_DELAYS_MS = [1000, 5000, 25_000];
|
|
9
|
+
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
10
|
+
const timer = setTimeout(resolve, ms);
|
|
11
|
+
timer.unref?.();
|
|
12
|
+
});
|
|
13
|
+
export class Webhooks {
|
|
14
|
+
url;
|
|
15
|
+
retryDelaysMs;
|
|
16
|
+
fetchImpl;
|
|
17
|
+
onError;
|
|
18
|
+
sleep;
|
|
19
|
+
/** Resolves when every in-flight delivery has settled (tests await this). */
|
|
20
|
+
inflight = new Set();
|
|
21
|
+
closed = false;
|
|
22
|
+
/** Resolvers that cut a retry sleep short when the server shuts down. */
|
|
23
|
+
sleepAborts = new Set();
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.url = options.url;
|
|
26
|
+
this.retryDelaysMs = options.retryDelaysMs ?? RETRY_DELAYS_MS;
|
|
27
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
28
|
+
this.onError = options.onError;
|
|
29
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
30
|
+
}
|
|
31
|
+
setUrl(url) {
|
|
32
|
+
this.url = url;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Abandon pending retries and wake anything sleeping between them. Shutdown
|
|
36
|
+
* must not wait 31 s for a dead receiver, and a webhook is fire-and-forget:
|
|
37
|
+
* an undelivered event is not an error.
|
|
38
|
+
*/
|
|
39
|
+
close() {
|
|
40
|
+
this.closed = true;
|
|
41
|
+
for (const abort of [...this.sleepAborts])
|
|
42
|
+
abort();
|
|
43
|
+
this.sleepAborts.clear();
|
|
44
|
+
}
|
|
45
|
+
get configured() {
|
|
46
|
+
return Boolean(this.url);
|
|
47
|
+
}
|
|
48
|
+
/** Queue a delivery. Returns immediately; never rejects. */
|
|
49
|
+
deliver(event, payload, at = Date.now()) {
|
|
50
|
+
if (!this.url || this.closed)
|
|
51
|
+
return;
|
|
52
|
+
const task = this.send({ event, payload, at }).catch(() => undefined);
|
|
53
|
+
this.inflight.add(task);
|
|
54
|
+
void task.finally(() => this.inflight.delete(task));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Wait for in-flight deliveries. Once closed there is nothing worth waiting
|
|
58
|
+
* for: retries have been abandoned and any request still on the wire is
|
|
59
|
+
* fire-and-forget, so shutdown returns immediately.
|
|
60
|
+
*/
|
|
61
|
+
async drain() {
|
|
62
|
+
if (this.closed)
|
|
63
|
+
return;
|
|
64
|
+
while (this.inflight.size > 0 && !this.closed) {
|
|
65
|
+
await Promise.all([...this.inflight]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** A retry delay that `close()` can cut short. */
|
|
69
|
+
async waitBeforeRetry(ms) {
|
|
70
|
+
if (this.closed)
|
|
71
|
+
return;
|
|
72
|
+
await new Promise((resolve) => {
|
|
73
|
+
const abort = () => {
|
|
74
|
+
this.sleepAborts.delete(abort);
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
this.sleepAborts.add(abort);
|
|
78
|
+
void this.sleep(ms).then(abort);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async send(delivery) {
|
|
82
|
+
const url = this.url;
|
|
83
|
+
if (!url)
|
|
84
|
+
return;
|
|
85
|
+
const body = JSON.stringify(delivery);
|
|
86
|
+
const attempts = this.retryDelaysMs.length + 1;
|
|
87
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
88
|
+
if (this.closed)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.fetchImpl(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body,
|
|
95
|
+
});
|
|
96
|
+
if (response.ok)
|
|
97
|
+
return;
|
|
98
|
+
throw new Error(`webhook responded ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.onError?.(err, attempt);
|
|
102
|
+
const delay = this.retryDelaysMs[attempt];
|
|
103
|
+
if (delay === undefined)
|
|
104
|
+
return;
|
|
105
|
+
await this.waitBeforeRetry(delay);
|
|
106
|
+
if (this.closed)
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
|