@rb2b/rb2b-apis-mcp 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RB2B
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @rb2b/rb2b-apis-mcp
2
+
3
+ MCP (Model Context Protocol) server that exposes [RB2B API's](https://ui.api.rb2b.com) identity resolution and enrichment API as tools for Claude and other MCP-compatible AI assistants.
4
+
5
+ Resolve IP addresses to companies, look up LinkedIn profiles from emails, find contact information from LinkedIn slugs, and more — all without leaving your AI assistant.
6
+
7
+ ---
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 18+
12
+ - An [RB2B APIs](https://ui.api.rb2b.com) account and API key. Don't have an account? Get your first 100 credits for just $9.
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Initialize
19
+
20
+ Run the setup wizard to configure your API key:
21
+
22
+ ```bash
23
+ npx @rb2b/rb2b-apis-mcp init
24
+ ```
25
+
26
+ This will prompt for your RB2B API key, validate it against the API, and store it securely in `~/.rb2b/config.json` (permissions: `600`).
27
+
28
+ ### 2. Add to Claude Desktop
29
+
30
+ Edit your Claude Desktop config file:
31
+
32
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
33
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "rb2b": {
39
+ "command": "npx",
40
+ "args": ["@rb2b/rb2b-apis-mcp"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ Restart Claude Desktop after saving.
47
+
48
+ ### 3. Add to Claude Code
49
+
50
+ ```bash
51
+ claude mcp add rb2b -- npx @rb2b/rb2b-apis-mcp
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Tools
57
+
58
+ Each API call deducts credits from your account balance. Free tools never consume credits. The server checks your balance before every paid call and will refuse the request if you have insufficient credits.
59
+
60
+ **Credit warnings** are automatically appended to responses when your balance falls to 500, 200, 100, 50, or 10 credits. Severity levels: `[NOTICE]` (500–51), `[WARNING]` (50–11), `[CRITICAL]` (10 and below).
61
+
62
+ **Rate limit:** 50 requests/second per endpoint. The server surfaces a clear error if this is exceeded.
63
+
64
+ ### Account
65
+
66
+ | Tool | Inputs | Cost | Description |
67
+ |------|--------|------|-------------|
68
+ | `help` | — | free | List all available tools with descriptions and costs |
69
+ | `check_credits` | — | free | Check remaining API credits for the account |
70
+ | `set_api_key` | `api_key` | free | Update the stored API key (validates before saving) |
71
+
72
+ ### IP Address Lookup
73
+
74
+ | Tool | Inputs | Cost | Description |
75
+ |------|--------|------|-------------|
76
+ | `ip_to_company` | `ip_address` | 1 credit | Identify the company associated with an IP address |
77
+ | `ip_to_hem` | `ip_address` | 1 credit | Get hashed emails (MD5 + SHA256) linked to an IP address |
78
+ | `ip_to_maid` | `ip_address` | 1 credit | Get Mobile Advertising IDs (MAIDs) linked to an IP address |
79
+
80
+ ### Email → Identity
81
+
82
+ | Tool | Inputs | Cost | Description |
83
+ |------|--------|------|-------------|
84
+ | `email_to_linkedin_slug` | `email` | 1 credit | Get the LinkedIn slug for a plain-text email |
85
+ | `email_to_best_linkedin` | `email` | 1 credit | Get the highest-confidence LinkedIn profile URL for an email |
86
+ | `email_to_business_profile` | `email` | 2 credits | Get business/employer profile (title, company, industry) for an email |
87
+ | `email_to_maid` | `email` | 1 credit | Get Mobile Advertising IDs linked to an email |
88
+
89
+ ### Hashed Email (HEM) → Identity
90
+
91
+ Accepts MD5 hashes of email addresses.
92
+
93
+ | Tool | Inputs | Cost | Description |
94
+ |------|--------|------|-------------|
95
+ | `hem_to_linkedin_slug` | `md5` | 1 credit | Get the LinkedIn slug for a hashed email |
96
+ | `hem_to_best_linkedin` | `md5` | 1 credit | Get the highest-confidence LinkedIn profile URL for a hashed email |
97
+ | `hem_to_business_profile` | `md5` | 2 credits | Get business/employer profile for a hashed email |
98
+ | `hem_to_maid` | `md5` | 1 credit | Get Mobile Advertising IDs linked to a hashed email |
99
+
100
+ ### LinkedIn → Identity
101
+
102
+ Accepts LinkedIn vanity URL slugs (e.g. `john-doe-123` from `linkedin.com/in/john-doe-123`).
103
+
104
+ | Tool | Inputs | Cost | Description |
105
+ |------|--------|------|-------------|
106
+ | `linkedin_to_hashed_emails` | `linkedin_slug` | 1 credit | Get all hashed emails (personal + business, MD5 + SHA256) for a profile |
107
+ | `linkedin_to_best_personal_email` | `linkedin_slug` | 1 credit | Get the highest-confidence personal email for a profile |
108
+ | `linkedin_to_personal_email` | `linkedin_slug` | 1 credit | Get all personal email candidates for a profile |
109
+ | `linkedin_to_mobile_phone` | `linkedin_slug` | 3 credits | Get the mobile phone number for a profile |
110
+ | `linkedin_to_business_profile` | `linkedin_slug` | 4 credits | Get business/employer profile (title, company, industry) for a profile |
111
+
112
+ ---
113
+
114
+ ## Configuration
115
+
116
+ The API key is stored at `~/.rb2b/config.json` with permissions `600` (owner read/write only):
117
+
118
+ ```json
119
+ {
120
+ "apiKey": "your-api-key-here"
121
+ }
122
+ ```
123
+
124
+ **To update your API key**, use any of these methods:
125
+
126
+ - **From Claude:** ask Claude to use the `set_api_key` tool — it validates the key before saving and takes effect immediately, no restart needed
127
+ - **From the terminal:** run `npx @rb2b/rb2b-apis-mcp init` — detects an existing key and prompts for a replacement
128
+
129
+ ---
130
+
131
+ ## Example Usage
132
+
133
+ Once the server is running, you can ask Claude things like:
134
+
135
+ - *"Who is visiting my site from IP 203.0.113.42?"*
136
+ - *"Find the LinkedIn profile for jane@example.com"*
137
+ - *"What company does robbclarke work at?"*
138
+ - *"Get the personal email for LinkedIn profile john-doe-123"*
139
+ - *"Check my RB2B credit balance"*
140
+
141
+ Claude will automatically select and call the appropriate tool.
142
+
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export declare class RB2BClient {
2
+ private apiKey;
3
+ constructor(apiKey: string);
4
+ setApiKey(apiKey: string): void;
5
+ private request;
6
+ checkCredits(): Promise<unknown>;
7
+ ipToCompany(ip_address: string): Promise<unknown>;
8
+ hemToBestLinkedin(md5: string): Promise<unknown>;
9
+ hemToBusinessProfile(md5: string): Promise<unknown>;
10
+ hemToLinkedinSlug(md5: string): Promise<unknown>;
11
+ hemToMaid(md5: string): Promise<unknown>;
12
+ emailToBestLinkedin(email: string): Promise<unknown>;
13
+ emailToBusinessProfile(email: string): Promise<unknown>;
14
+ emailToLinkedinSlug(email: string): Promise<unknown>;
15
+ emailToMaid(email: string): Promise<unknown>;
16
+ linkedinToBestPersonalEmail(linkedin_slug: string): Promise<unknown>;
17
+ linkedinToPersonalEmail(linkedin_slug: string): Promise<unknown>;
18
+ linkedinToMobilePhone(linkedin_slug: string): Promise<unknown>;
19
+ linkedinToBusinessProfile(linkedin_slug: string): Promise<unknown>;
20
+ ipToHem(ip_address: string): Promise<unknown>;
21
+ ipToMaid(ip_address: string): Promise<unknown>;
22
+ linkedinToHashedEmails(linkedin_slug: string): Promise<unknown>;
23
+ }
package/dist/api.js ADDED
@@ -0,0 +1,101 @@
1
+ import { BASE_URL } from "./config.js";
2
+ export class RB2BClient {
3
+ apiKey;
4
+ constructor(apiKey) {
5
+ this.apiKey = apiKey;
6
+ }
7
+ setApiKey(apiKey) {
8
+ this.apiKey = apiKey;
9
+ }
10
+ async request(method, path, body) {
11
+ const url = `${BASE_URL}${path}`;
12
+ const headers = {
13
+ "Api-Key": this.apiKey,
14
+ "Content-Type": "application/json",
15
+ };
16
+ let res;
17
+ try {
18
+ res = await fetch(url, {
19
+ method,
20
+ headers,
21
+ body: body ? JSON.stringify(body) : undefined,
22
+ });
23
+ }
24
+ catch (err) {
25
+ throw new Error(`Unable to reach the RB2B API. Please check your network connection. (${err instanceof Error ? err.message : String(err)})`);
26
+ }
27
+ if (res.status === 401) {
28
+ throw new Error("Invalid API key. Run: npx @rb2b/rb2b-apis-mcp init to update it.");
29
+ }
30
+ if (res.status === 404) {
31
+ throw new Error("No record found for the provided input.");
32
+ }
33
+ if (res.status === 429) {
34
+ throw new Error("Rate limit exceeded (50 requests/second per endpoint). Please wait before retrying.");
35
+ }
36
+ if (!res.ok) {
37
+ const text = await res.text().catch(() => res.statusText);
38
+ throw new Error(`RB2B API error ${res.status}: ${text}`);
39
+ }
40
+ return res.json();
41
+ }
42
+ checkCredits() {
43
+ return this.request("GET", "/credits");
44
+ }
45
+ ipToCompany(ip_address) {
46
+ return this.request("POST", "/ip_to_company", { ip_address });
47
+ }
48
+ hemToBestLinkedin(md5) {
49
+ return this.request("POST", "/hem_to_best_linkedin", { md5 });
50
+ }
51
+ hemToBusinessProfile(md5) {
52
+ return this.request("POST", "/hem_to_business_profile", { md5 });
53
+ }
54
+ hemToLinkedinSlug(md5) {
55
+ return this.request("POST", "/hem_to_linkedin", { md5 });
56
+ }
57
+ hemToMaid(md5) {
58
+ return this.request("POST", "/hem_to_maid", { md5 });
59
+ }
60
+ emailToBestLinkedin(email) {
61
+ return this.request("POST", "/hem_to_best_linkedin", { email });
62
+ }
63
+ emailToBusinessProfile(email) {
64
+ return this.request("POST", "/hem_to_business_profile", { email });
65
+ }
66
+ emailToLinkedinSlug(email) {
67
+ return this.request("POST", "/hem_to_linkedin", { email });
68
+ }
69
+ emailToMaid(email) {
70
+ return this.request("POST", "/hem_to_maid", { email });
71
+ }
72
+ linkedinToBestPersonalEmail(linkedin_slug) {
73
+ return this.request("POST", "/linkedin_to_best_personal_email", {
74
+ linkedin_slug,
75
+ });
76
+ }
77
+ linkedinToPersonalEmail(linkedin_slug) {
78
+ return this.request("POST", "/linkedin_to_personal_email", {
79
+ linkedin_slug,
80
+ });
81
+ }
82
+ linkedinToMobilePhone(linkedin_slug) {
83
+ return this.request("POST", "/linkedin_to_mobile_phone", {
84
+ linkedin_slug,
85
+ });
86
+ }
87
+ linkedinToBusinessProfile(linkedin_slug) {
88
+ return this.request("POST", "/linkedin_to_business_profile", {
89
+ linkedin_slug,
90
+ });
91
+ }
92
+ ipToHem(ip_address) {
93
+ return this.request("POST", "/ip_to_hem", { ip_address });
94
+ }
95
+ ipToMaid(ip_address) {
96
+ return this.request("POST", "/ip_to_maid", { ip_address });
97
+ }
98
+ linkedinToHashedEmails(linkedin_slug) {
99
+ return this.request("POST", "/linkedin_to_hashed_emails", { linkedin_slug });
100
+ }
101
+ }
@@ -0,0 +1,6 @@
1
+ export interface Config {
2
+ apiKey: string;
3
+ }
4
+ export declare function loadConfig(): Config | null;
5
+ export declare function saveConfig(config: Config): void;
6
+ export declare const BASE_URL = "https://api.rb2b.com/api/v1";
package/dist/config.js ADDED
@@ -0,0 +1,27 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ const CONFIG_DIR = join(homedir(), ".rb2b");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ export function loadConfig() {
7
+ if (!existsSync(CONFIG_FILE))
8
+ return null;
9
+ try {
10
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
11
+ const parsed = JSON.parse(raw);
12
+ if (typeof parsed.apiKey === "string" && parsed.apiKey.length > 0) {
13
+ return parsed;
14
+ }
15
+ return null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ export function saveConfig(config) {
22
+ if (!existsSync(CONFIG_DIR)) {
23
+ mkdirSync(CONFIG_DIR, { recursive: true });
24
+ }
25
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
26
+ }
27
+ export const BASE_URL = "https://api.rb2b.com/api/v1";
package/dist/init.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/init.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "readline";
3
+ import { BASE_URL, loadConfig, saveConfig } from "./config.js";
4
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
5
+ function prompt(question) {
6
+ return new Promise((resolve) => rl.question(question, resolve));
7
+ }
8
+ async function main() {
9
+ const existing = loadConfig();
10
+ if (existing) {
11
+ console.log("RB2B MCP Server — Update API Key");
12
+ console.log("==================================\n");
13
+ console.log("A key is already configured. Enter a new key to replace it.\n");
14
+ }
15
+ else {
16
+ console.log("RB2B MCP Server — Initial Setup");
17
+ console.log("=================================\n");
18
+ }
19
+ const apiKey = await prompt("Enter your RB2B API key: ");
20
+ if (!apiKey.trim()) {
21
+ console.error("Error: API key cannot be empty.");
22
+ process.exit(1);
23
+ }
24
+ console.log("\nValidating API key...");
25
+ try {
26
+ const res = await fetch(`${BASE_URL}/credits`, {
27
+ headers: { "Api-Key": apiKey.trim() },
28
+ });
29
+ if (!res.ok) {
30
+ const text = await res.text().catch(() => res.statusText);
31
+ console.error(`\nError: API key validation failed (${res.status}): ${text}`);
32
+ process.exit(1);
33
+ }
34
+ const data = await res.json();
35
+ console.log("\nAPI key valid!");
36
+ console.log("Credits info:", JSON.stringify(data, null, 2));
37
+ saveConfig({ apiKey: apiKey.trim() });
38
+ console.log("\nConfiguration saved to ~/.rb2b/config.json");
39
+ console.log("\nYou can now add the MCP server to your Claude config:\n");
40
+ console.log(JSON.stringify({
41
+ mcpServers: {
42
+ rb2b: {
43
+ command: "npx",
44
+ args: ["@rb2b/rb2b-apis-mcp"],
45
+ },
46
+ },
47
+ }, null, 2));
48
+ }
49
+ catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ console.error(`\nError: ${message}`);
52
+ process.exit(1);
53
+ }
54
+ finally {
55
+ rl.close();
56
+ }
57
+ }
58
+ main();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare const CREDIT_COSTS: Record<string, number>;
package/dist/server.js ADDED
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { createRequire } from "module";
6
+ import { loadConfig, saveConfig, BASE_URL } from "./config.js";
7
+ import { RB2BClient } from "./api.js";
8
+ import { creditLabel, getCreditWarning } from "./utils.js";
9
+ const require = createRequire(import.meta.url);
10
+ const { version } = require("../package.json");
11
+ const config = loadConfig();
12
+ if (!config) {
13
+ process.stderr.write("RB2B API key not configured. Run: npx @rb2b/rb2b-apis-mcp init\n");
14
+ process.exit(1);
15
+ }
16
+ // Mutable so set_api_key can update it without a server restart.
17
+ let client = new RB2BClient(config.apiKey);
18
+ // Credit cost per tool. 0 = free, undefined = treated as 0.
19
+ export const CREDIT_COSTS = {
20
+ help: 0,
21
+ set_api_key: 0,
22
+ check_credits: 0,
23
+ ip_to_company: 1,
24
+ ip_to_hem: 1,
25
+ ip_to_maid: 1,
26
+ email_to_linkedin_slug: 1,
27
+ email_to_best_linkedin: 1,
28
+ email_to_business_profile: 2,
29
+ email_to_maid: 1,
30
+ hem_to_linkedin_slug: 1,
31
+ hem_to_best_linkedin: 1,
32
+ hem_to_business_profile: 2,
33
+ hem_to_maid: 1,
34
+ linkedin_to_hashed_emails: 1,
35
+ linkedin_to_best_personal_email: 1,
36
+ linkedin_to_personal_email: 1,
37
+ linkedin_to_mobile_phone: 3,
38
+ linkedin_to_business_profile: 4,
39
+ };
40
+ const RATE_LIMIT_NOTE = "Rate limit: 50 requests/second per endpoint.";
41
+ const server = new Server({ name: "@rb2b/rb2b-apis-mcp", version }, { capabilities: { tools: {} } });
42
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
43
+ tools: [
44
+ {
45
+ name: "set_api_key",
46
+ description: "Update the RB2B API key. The new key is validated before saving and takes effect immediately. Cost: free.",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ api_key: {
51
+ type: "string",
52
+ description: "The new RB2B API key to save",
53
+ },
54
+ },
55
+ required: ["api_key"],
56
+ },
57
+ },
58
+ {
59
+ name: "help",
60
+ description: "List all available RB2B MCP tools with descriptions, required inputs, and credit costs. Use this to discover what the RB2B MCP server can do. Cost: free.",
61
+ inputSchema: { type: "object", properties: {}, required: [] },
62
+ },
63
+ {
64
+ name: "check_credits",
65
+ description: "Check the remaining API credits for the current RB2B account. Use this to verify the API key is valid or monitor usage. Cost: free.",
66
+ inputSchema: { type: "object", properties: {}, required: [] },
67
+ },
68
+ {
69
+ name: "ip_to_company",
70
+ description: `Identify the company associated with a given IP address. Useful for de-anonymizing website visitors by their IP. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ ip_address: { type: "string", description: "The IPv4 or IPv6 address to look up" },
75
+ },
76
+ required: ["ip_address"],
77
+ },
78
+ },
79
+ {
80
+ name: "linkedin_to_hashed_emails",
81
+ description: `Retrieve all hashed emails (HEMs) associated with a LinkedIn profile slug. Returns personal and business MD5 and SHA256 arrays. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ linkedin_slug: { type: "string", description: "LinkedIn profile slug / vanity URL (e.g. 'john-doe-123')" },
86
+ },
87
+ required: ["linkedin_slug"],
88
+ },
89
+ },
90
+ {
91
+ name: "ip_to_maid",
92
+ description: `Retrieve Mobile Advertising IDs (MAIDs) associated with an IP address. Returns a list of device IDs with type (AAID for Android, IDFA for iOS). Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ ip_address: { type: "string", description: "The IPv4 or IPv6 address to look up" },
97
+ },
98
+ required: ["ip_address"],
99
+ },
100
+ },
101
+ {
102
+ name: "ip_to_hem",
103
+ description: `Retrieve hashed emails (HEMs) associated with an IP address. Returns a ranked list of MD5 and SHA256 hashes with confidence scores. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ ip_address: { type: "string", description: "The IPv4 or IPv6 address to look up" },
108
+ },
109
+ required: ["ip_address"],
110
+ },
111
+ },
112
+ {
113
+ name: "hem_to_best_linkedin",
114
+ description: `Find the best matching LinkedIn profile URL for a hashed email (MD5). Returns the most confident LinkedIn match for the given HEM. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ md5: { type: "string", description: "MD5 hash of the email address (hashed email / HEM)" },
119
+ },
120
+ required: ["md5"],
121
+ },
122
+ },
123
+ {
124
+ name: "hem_to_business_profile",
125
+ description: `Retrieve the business/company profile associated with a hashed email (MD5), including employer and job title information. Cost: 2 credits. ${RATE_LIMIT_NOTE}`,
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ md5: { type: "string", description: "MD5 hash of the email address (hashed email / HEM)" },
130
+ },
131
+ required: ["md5"],
132
+ },
133
+ },
134
+ {
135
+ name: "hem_to_linkedin_slug",
136
+ description: `Look up the LinkedIn profile slug (e.g. 'john-doe-123') for a hashed email (MD5). Returns the LinkedIn vanity URL slug. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ md5: { type: "string", description: "MD5 hash of the email address (hashed email / HEM)" },
141
+ },
142
+ required: ["md5"],
143
+ },
144
+ },
145
+ {
146
+ name: "hem_to_maid",
147
+ description: `Retrieve the Mobile Advertising ID (MAID) linked to a hashed email (MD5). Useful for cross-device identity resolution. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ md5: { type: "string", description: "MD5 hash of the email address (hashed email / HEM)" },
152
+ },
153
+ required: ["md5"],
154
+ },
155
+ },
156
+ {
157
+ name: "email_to_best_linkedin",
158
+ description: `Find the best matching LinkedIn profile URL for a plain-text email address. Returns the most confident LinkedIn match. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ email: { type: "string", description: "Plain-text email address to look up" },
163
+ },
164
+ required: ["email"],
165
+ },
166
+ },
167
+ {
168
+ name: "email_to_business_profile",
169
+ description: `Retrieve the business/company profile associated with a plain-text email address, including employer and job title. Cost: 2 credits. ${RATE_LIMIT_NOTE}`,
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ email: { type: "string", description: "Plain-text email address to look up" },
174
+ },
175
+ required: ["email"],
176
+ },
177
+ },
178
+ {
179
+ name: "email_to_linkedin_slug",
180
+ description: `Look up the LinkedIn profile slug for a plain-text email address. Returns the LinkedIn vanity URL slug. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ email: { type: "string", description: "Plain-text email address to look up" },
185
+ },
186
+ required: ["email"],
187
+ },
188
+ },
189
+ {
190
+ name: "email_to_maid",
191
+ description: `Retrieve the Mobile Advertising ID (MAID) linked to a plain-text email address. Useful for cross-device identity resolution. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ email: { type: "string", description: "Plain-text email address to look up" },
196
+ },
197
+ required: ["email"],
198
+ },
199
+ },
200
+ {
201
+ name: "linkedin_to_best_personal_email",
202
+ description: `Find the best personal email address for a LinkedIn profile slug. Returns the highest-confidence personal email match. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ linkedin_slug: { type: "string", description: "LinkedIn profile slug / vanity URL (e.g. 'john-doe-123')" },
207
+ },
208
+ required: ["linkedin_slug"],
209
+ },
210
+ },
211
+ {
212
+ name: "linkedin_to_personal_email",
213
+ description: `Retrieve personal email addresses associated with a LinkedIn profile slug. May return multiple candidate emails. Cost: 1 credit. ${RATE_LIMIT_NOTE}`,
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ linkedin_slug: { type: "string", description: "LinkedIn profile slug / vanity URL (e.g. 'john-doe-123')" },
218
+ },
219
+ required: ["linkedin_slug"],
220
+ },
221
+ },
222
+ {
223
+ name: "linkedin_to_mobile_phone",
224
+ description: `Look up the mobile phone number associated with a LinkedIn profile slug. Returns a mobile number when available. Cost: 3 credits. ${RATE_LIMIT_NOTE}`,
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ linkedin_slug: { type: "string", description: "LinkedIn profile slug / vanity URL (e.g. 'john-doe-123')" },
229
+ },
230
+ required: ["linkedin_slug"],
231
+ },
232
+ },
233
+ {
234
+ name: "linkedin_to_business_profile",
235
+ description: `Retrieve the business/company profile for a LinkedIn profile slug, including current employer and role details. Cost: 4 credits. ${RATE_LIMIT_NOTE}`,
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ linkedin_slug: { type: "string", description: "LinkedIn profile slug / vanity URL (e.g. 'john-doe-123')" },
240
+ },
241
+ required: ["linkedin_slug"],
242
+ },
243
+ },
244
+ ],
245
+ }));
246
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
247
+ const { name, arguments: args } = req.params;
248
+ const a = (args ?? {});
249
+ try {
250
+ let result;
251
+ let remaining = 0;
252
+ // Pre-flight credit check for all paid tools.
253
+ const cost = CREDIT_COSTS[name] ?? 0;
254
+ if (cost > 0) {
255
+ let creditsData;
256
+ try {
257
+ creditsData = await client.checkCredits();
258
+ }
259
+ catch (err) {
260
+ // 401 is already formatted as a friendly message by RB2BClient.
261
+ throw err;
262
+ }
263
+ remaining = creditsData?.credits_remaining ?? 0;
264
+ if (remaining < cost) {
265
+ return {
266
+ content: [{
267
+ type: "text",
268
+ text: `Insufficient credits. "${name}" costs ${cost} credit${cost === 1 ? "" : "s"} but you only have ${remaining} remaining. Top up at https://ui.api.rb2b.com.`,
269
+ }],
270
+ isError: true,
271
+ };
272
+ }
273
+ }
274
+ switch (name) {
275
+ case "set_api_key": {
276
+ const newKey = a.api_key?.trim();
277
+ if (!newKey) {
278
+ return {
279
+ content: [{ type: "text", text: "Error: api_key cannot be empty." }],
280
+ isError: true,
281
+ };
282
+ }
283
+ const res = await fetch(`${BASE_URL}/credits`, {
284
+ headers: { "Api-Key": newKey },
285
+ });
286
+ if (!res.ok) {
287
+ const text = await res.text().catch(() => res.statusText);
288
+ return {
289
+ content: [{ type: "text", text: `Error: API key validation failed (${res.status}): ${text}` }],
290
+ isError: true,
291
+ };
292
+ }
293
+ saveConfig({ apiKey: newKey });
294
+ client.setApiKey(newKey);
295
+ result = {
296
+ success: true,
297
+ message: "API key updated and active immediately.",
298
+ };
299
+ break;
300
+ }
301
+ case "help":
302
+ result = {
303
+ description: "RB2B MCP Server — Identity Resolution & Enrichment Tools",
304
+ rate_limit: "50 requests/second per endpoint. When processing batches, stay within this limit.",
305
+ tools: [
306
+ { name: "help", cost: creditLabel(0), input: "none", description: "List all available tools (this command)" },
307
+ { name: "set_api_key", cost: creditLabel(0), input: "api_key", description: "Update the RB2B API key (validates before saving, takes effect immediately)" },
308
+ { name: "check_credits", cost: creditLabel(0), input: "none", description: "Check remaining API credits" },
309
+ { name: "ip_to_company", cost: creditLabel(1), input: "ip_address", description: "Identify the company behind an IP address" },
310
+ { name: "ip_to_hem", cost: creditLabel(1), input: "ip_address", description: "Get hashed emails (MD5 + SHA256) for an IP address" },
311
+ { name: "ip_to_maid", cost: creditLabel(1), input: "ip_address", description: "Get Mobile Advertising IDs (MAIDs) for an IP address" },
312
+ { name: "email_to_linkedin_slug", cost: creditLabel(1), input: "email", description: "Get the LinkedIn slug for a plain-text email" },
313
+ { name: "email_to_best_linkedin", cost: creditLabel(1), input: "email", description: "Get the best matching LinkedIn URL for an email" },
314
+ { name: "email_to_business_profile", cost: creditLabel(2), input: "email", description: "Get business/company profile for an email" },
315
+ { name: "email_to_maid", cost: creditLabel(1), input: "email", description: "Get Mobile Advertising IDs for an email" },
316
+ { name: "hem_to_linkedin_slug", cost: creditLabel(1), input: "md5", description: "Get the LinkedIn slug for a hashed email (MD5)" },
317
+ { name: "hem_to_best_linkedin", cost: creditLabel(1), input: "md5", description: "Get the best matching LinkedIn URL for a hashed email" },
318
+ { name: "hem_to_business_profile", cost: creditLabel(2), input: "md5", description: "Get business/company profile for a hashed email" },
319
+ { name: "hem_to_maid", cost: creditLabel(1), input: "md5", description: "Get Mobile Advertising IDs for a hashed email" },
320
+ { name: "linkedin_to_hashed_emails", cost: creditLabel(1), input: "linkedin_slug", description: "Get all hashed emails (HEMs) for a LinkedIn profile" },
321
+ { name: "linkedin_to_best_personal_email", cost: creditLabel(1), input: "linkedin_slug", description: "Get the best personal email for a LinkedIn profile" },
322
+ { name: "linkedin_to_personal_email", cost: creditLabel(1), input: "linkedin_slug", description: "Get all personal emails for a LinkedIn profile" },
323
+ { name: "linkedin_to_mobile_phone", cost: creditLabel(3), input: "linkedin_slug", description: "Get the mobile phone number for a LinkedIn profile" },
324
+ { name: "linkedin_to_business_profile", cost: creditLabel(4), input: "linkedin_slug", description: "Get business/company profile for a LinkedIn profile" },
325
+ ],
326
+ examples: [
327
+ "Who is visiting my site from IP 203.0.113.42?",
328
+ "Find the LinkedIn profile for jane@example.com",
329
+ "What company does john-doe-123 work at?",
330
+ "Get the personal email for LinkedIn profile john-doe-123",
331
+ "What's the mobile number for linkedin.com/in/john-doe-123?",
332
+ "Find LinkedIn profiles for all the emails in this spreadsheet",
333
+ "Look up the business profile for the MD5 hash abc123...",
334
+ "Check my remaining API credits",
335
+ "Update my API key to a new value",
336
+ ],
337
+ };
338
+ break;
339
+ case "check_credits": {
340
+ let creditsResult;
341
+ try {
342
+ creditsResult = await client.checkCredits();
343
+ }
344
+ catch (err) {
345
+ const message = err instanceof Error ? err.message : String(err);
346
+ return {
347
+ content: [{ type: "text", text: message }],
348
+ isError: true,
349
+ };
350
+ }
351
+ const balance = creditsResult?.credits_remaining ?? 0;
352
+ const checkWarning = getCreditWarning(balance);
353
+ const checkContent = [
354
+ { type: "text", text: JSON.stringify(creditsResult, null, 2) },
355
+ ];
356
+ if (checkWarning) {
357
+ checkContent.push({ type: "text", text: checkWarning });
358
+ }
359
+ return { content: checkContent };
360
+ }
361
+ case "ip_to_company":
362
+ result = await client.ipToCompany(a.ip_address);
363
+ break;
364
+ case "hem_to_best_linkedin":
365
+ result = await client.hemToBestLinkedin(a.md5);
366
+ break;
367
+ case "hem_to_business_profile":
368
+ result = await client.hemToBusinessProfile(a.md5);
369
+ break;
370
+ case "hem_to_linkedin_slug":
371
+ result = await client.hemToLinkedinSlug(a.md5);
372
+ break;
373
+ case "hem_to_maid":
374
+ result = await client.hemToMaid(a.md5);
375
+ break;
376
+ case "email_to_best_linkedin":
377
+ result = await client.emailToBestLinkedin(a.email);
378
+ break;
379
+ case "email_to_business_profile":
380
+ result = await client.emailToBusinessProfile(a.email);
381
+ break;
382
+ case "email_to_linkedin_slug":
383
+ result = await client.emailToLinkedinSlug(a.email);
384
+ break;
385
+ case "email_to_maid":
386
+ result = await client.emailToMaid(a.email);
387
+ break;
388
+ case "linkedin_to_best_personal_email":
389
+ result = await client.linkedinToBestPersonalEmail(a.linkedin_slug);
390
+ break;
391
+ case "linkedin_to_personal_email":
392
+ result = await client.linkedinToPersonalEmail(a.linkedin_slug);
393
+ break;
394
+ case "linkedin_to_mobile_phone":
395
+ result = await client.linkedinToMobilePhone(a.linkedin_slug);
396
+ break;
397
+ case "linkedin_to_business_profile":
398
+ result = await client.linkedinToBusinessProfile(a.linkedin_slug);
399
+ break;
400
+ case "ip_to_hem":
401
+ result = await client.ipToHem(a.ip_address);
402
+ break;
403
+ case "ip_to_maid":
404
+ result = await client.ipToMaid(a.ip_address);
405
+ break;
406
+ case "linkedin_to_hashed_emails":
407
+ result = await client.linkedinToHashedEmails(a.linkedin_slug);
408
+ break;
409
+ default:
410
+ return {
411
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
412
+ isError: true,
413
+ };
414
+ }
415
+ const content = [
416
+ { type: "text", text: JSON.stringify(result, null, 2) },
417
+ ];
418
+ // Only warn when we actually fetched the balance (paid tools).
419
+ if (cost > 0) {
420
+ const warning = getCreditWarning(remaining - cost);
421
+ if (warning) {
422
+ content.push({ type: "text", text: warning });
423
+ }
424
+ }
425
+ return { content };
426
+ }
427
+ catch (err) {
428
+ const message = err instanceof Error ? err.message : String(err);
429
+ return {
430
+ content: [{ type: "text", text: message }],
431
+ isError: true,
432
+ };
433
+ }
434
+ });
435
+ const transport = new StdioServerTransport();
436
+ await server.connect(transport);
@@ -0,0 +1,3 @@
1
+ export declare const CREDIT_WARNING_THRESHOLDS: number[];
2
+ export declare function creditLabel(cost: number): string;
3
+ export declare function getCreditWarning(balance: number): string | null;
package/dist/utils.js ADDED
@@ -0,0 +1,14 @@
1
+ // Warn at these credit thresholds (checked against post-call balance).
2
+ export const CREDIT_WARNING_THRESHOLDS = [500, 200, 100, 50, 10];
3
+ export function creditLabel(cost) {
4
+ return cost === 0 ? "free" : `${cost} credit${cost === 1 ? "" : "s"}`;
5
+ }
6
+ export function getCreditWarning(balance) {
7
+ for (const threshold of CREDIT_WARNING_THRESHOLDS) {
8
+ if (balance <= threshold) {
9
+ const level = balance <= 10 ? "CRITICAL" : balance <= 50 ? "WARNING" : "NOTICE";
10
+ return `[${level}] ${balance} credits remaining. Top up at https://ui.api.rb2b.com.`;
11
+ }
12
+ }
13
+ return null;
14
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rb2b/rb2b-apis-mcp",
3
+ "version": "1.1.0",
4
+ "description": "MCP server exposing RB2B API tools for identity resolution and enrichment",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "rb2b-mcp": "dist/server.js",
9
+ "rb2b-mcp-init": "dist/init.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "start": "node dist/server.js",
15
+ "init": "node dist/init.js",
16
+ "test": "npm run build && tsc -p tsconfig.test.json && node --test dist-test/utils.test.js",
17
+ "prepare": "npm run build"
18
+ },
19
+ "files": [
20
+ "dist/**/*",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "rb2b",
27
+ "identity-resolution",
28
+ "enrichment",
29
+ "linkedin",
30
+ "email",
31
+ "ip-lookup"
32
+ ],
33
+ "author": "RB2B",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/robbclarke/RB2B-APIs-MCP.git"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "typescript": "^5.6.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ }
52
+ }