@radsoft/markbase-mcp 2026.8.1
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 +75 -0
- package/dist/client.js +104 -0
- package/dist/index.js +155 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Markbase MCP
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server for the [Markbase](https://markbase.co)
|
|
4
|
+
USPTO trademark API. Gives an assistant the ability to search US trademarks, screen a
|
|
5
|
+
proposed brand name for conflicts, and pull registration records — grounded in the
|
|
6
|
+
actual federal register instead of guessed from training data.
|
|
7
|
+
|
|
8
|
+
Covers the **USPTO only** (14M+ US marks). If you need EUIPO, WIPO, or UKIPO, a
|
|
9
|
+
multi-office platform is a better fit.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
You need a free API key — [get one at markbase.co](https://markbase.co/login).
|
|
14
|
+
5,000 requests a month, no card.
|
|
15
|
+
|
|
16
|
+
Add to your MCP client config:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"markbase": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "@radsoft/markbase-mcp"],
|
|
24
|
+
"env": {
|
|
25
|
+
"MARKBASE_API_KEY": "mb_your_key_here"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For Claude Code:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add markbase --env MARKBASE_API_KEY=mb_your_key -- npx -y @radsoft/markbase-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
| Tool | What it does |
|
|
41
|
+
|------|--------------|
|
|
42
|
+
| `check_trademark_availability` | Screen a proposed name against the register, ranked by similarity with a risk level |
|
|
43
|
+
| `search_trademarks` | Typo-tolerant search across 14M+ marks, filterable by class and status |
|
|
44
|
+
| `get_trademark` | Full record for one mark by serial number |
|
|
45
|
+
| `get_trademark_timeline` | Prosecution history — filing, publication, office actions, registration |
|
|
46
|
+
| `search_by_owner` | Every mark held by a company or person |
|
|
47
|
+
| `list_trademark_classes` | All 45 Nice classes, so the model can pick the right one before screening |
|
|
48
|
+
|
|
49
|
+
## Why the class matters
|
|
50
|
+
|
|
51
|
+
Trademark rights are scoped to goods and services. DELTA coexists as an airline, a
|
|
52
|
+
faucet maker, and dental insurance. Screening a name without a class produces noise —
|
|
53
|
+
pass `trademark_class` and you get only marks that could actually block you.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
check_trademark_availability({ name: "Acme", trademark_class: "9" })
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Not legal advice
|
|
60
|
+
|
|
61
|
+
These tools return a first-pass screening signal over the federal register. They do
|
|
62
|
+
not cover state trademarks, common-law rights, or the legal analysis of likelihood
|
|
63
|
+
of confusion. Treat a clean result as "worth pursuing", not "cleared", and recommend
|
|
64
|
+
a trademark attorney for any name a user intends to actually adopt.
|
|
65
|
+
|
|
66
|
+
## Environment
|
|
67
|
+
|
|
68
|
+
| Variable | Required | Default |
|
|
69
|
+
|----------|----------|---------|
|
|
70
|
+
| `MARKBASE_API_KEY` | yes | — |
|
|
71
|
+
| `MARKBASE_API_URL` | no | `https://api.markbase.co` |
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT — a product of [Rad Soft](https://radsoftinc.com).
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const BASE = process.env.MARKBASE_API_URL || "https://api.markbase.co";
|
|
2
|
+
const KEY = process.env.MARKBASE_API_KEY;
|
|
3
|
+
// the api is public, so this isn't a lock — it's so every mcp user is a real
|
|
4
|
+
// account we can see and support, instead of anonymous traffic on a shared ip quota
|
|
5
|
+
export function requireKey() {
|
|
6
|
+
if (!KEY) {
|
|
7
|
+
throw new Error("MARKBASE_API_KEY is not set. Get a free key at https://markbase.co/login " +
|
|
8
|
+
"(5,000 requests/month, no card), then add it to your MCP client config as " +
|
|
9
|
+
'an env var: { "env": { "MARKBASE_API_KEY": "mb_..." } }');
|
|
10
|
+
}
|
|
11
|
+
return KEY;
|
|
12
|
+
}
|
|
13
|
+
// lets us measure how much traffic the mcp actually drives, separately from
|
|
14
|
+
// direct api use, in api_requests
|
|
15
|
+
const UA = "markbase-mcp/0.1.0";
|
|
16
|
+
export class MarkbaseError extends Error {
|
|
17
|
+
status;
|
|
18
|
+
constructor(message, status) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function request(path, init) {
|
|
24
|
+
const key = requireKey();
|
|
25
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
26
|
+
...init,
|
|
27
|
+
headers: {
|
|
28
|
+
accept: "application/json",
|
|
29
|
+
"user-agent": UA,
|
|
30
|
+
...(init?.body ? { "content-type": "application/json" } : {}),
|
|
31
|
+
"x-api-key": key,
|
|
32
|
+
...init?.headers,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = text ? JSON.parse(text) : {};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
body = { error: text.slice(0, 200) };
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
// surface quota state, since the most common failure on the keyless tier is
|
|
45
|
+
// running out of month rather than anything the caller did wrong
|
|
46
|
+
const remaining = res.headers.get("x-quota-remaining");
|
|
47
|
+
const detail = res.status === 401
|
|
48
|
+
? "Invalid MARKBASE_API_KEY. Get a key at https://markbase.co/login"
|
|
49
|
+
: res.status === 429 && remaining === "0"
|
|
50
|
+
? `${body.error || "monthly quota exceeded"}. Your plan resets monthly — see https://markbase.co/#pricing to raise it.`
|
|
51
|
+
: body.error || `request failed (${res.status})`;
|
|
52
|
+
throw new MarkbaseError(detail, res.status);
|
|
53
|
+
}
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
export function padClass(v) {
|
|
57
|
+
return /^\d+$/.test(v) ? v.padStart(3, "0") : v;
|
|
58
|
+
}
|
|
59
|
+
// numeric USPTO status codes are meaningless to a model without this
|
|
60
|
+
export function statusLabel(code) {
|
|
61
|
+
if (!code)
|
|
62
|
+
return "unknown";
|
|
63
|
+
const n = parseInt(code, 10);
|
|
64
|
+
if (Number.isNaN(n))
|
|
65
|
+
return "unknown";
|
|
66
|
+
if (n >= 600 && n < 700)
|
|
67
|
+
return "dead";
|
|
68
|
+
if (n >= 710 && n < 720)
|
|
69
|
+
return "cancelled";
|
|
70
|
+
if (n >= 900)
|
|
71
|
+
return "expired";
|
|
72
|
+
if (n >= 800 || n === 700 || (n >= 730 && n < 740))
|
|
73
|
+
return "registered";
|
|
74
|
+
if (n < 600)
|
|
75
|
+
return "pending";
|
|
76
|
+
return "registered";
|
|
77
|
+
}
|
|
78
|
+
export function isoDate(d) {
|
|
79
|
+
if (!d || d.length !== 8)
|
|
80
|
+
return null;
|
|
81
|
+
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
|
|
82
|
+
}
|
|
83
|
+
// models do better with labelled fields than with raw uspto codes
|
|
84
|
+
export function shapeMark(m) {
|
|
85
|
+
// the register uses all-zero placeholders for "none"; a model will happily
|
|
86
|
+
// report "registration number 0000000" if we pass that through
|
|
87
|
+
const reg = m.registration_number && /[1-9]/.test(m.registration_number)
|
|
88
|
+
? m.registration_number
|
|
89
|
+
: null;
|
|
90
|
+
return {
|
|
91
|
+
serial_number: m.serial_number,
|
|
92
|
+
registration_number: reg,
|
|
93
|
+
mark: m.word_mark,
|
|
94
|
+
owner: m.owner_name,
|
|
95
|
+
owner_country: m.owner_country,
|
|
96
|
+
status: statusLabel(m.status_code),
|
|
97
|
+
status_code: m.status_code,
|
|
98
|
+
filed: isoDate(m.filing_date),
|
|
99
|
+
registered: isoDate(m.registration_date),
|
|
100
|
+
// a mark can list the same class twice across its filing basis rows
|
|
101
|
+
classes: [...new Set(m.international_codes?.map((c) => String(parseInt(c, 10))) ?? [])],
|
|
102
|
+
goods_services: m.goods_services,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { request, padClass, shapeMark, MarkbaseError } from "./client.js";
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "markbase",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
});
|
|
10
|
+
// every tool returns text content; errors come back as isError so the model can
|
|
11
|
+
// recover rather than seeing a transport failure
|
|
12
|
+
function ok(data) {
|
|
13
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
14
|
+
}
|
|
15
|
+
function fail(err) {
|
|
16
|
+
const msg = err instanceof MarkbaseError ? err.message : err.message;
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: `Markbase error: ${msg}` }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
server.registerTool("search_trademarks", {
|
|
23
|
+
title: "Search US trademarks",
|
|
24
|
+
description: "Full-text, typo-tolerant search across 14M+ USPTO trademark records. Use this to find existing marks by name. Filter by class to scope to relevant goods/services, and by status to exclude dead marks.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
query: z.string().describe("Mark name or text to search for, e.g. 'nike'"),
|
|
27
|
+
trademark_class: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Nice class 1-45 to scope results, e.g. '9' for software"),
|
|
31
|
+
live_only: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Only return registered/live marks (status 800). Defaults to false."),
|
|
35
|
+
limit: z.number().int().min(1).max(25).optional().describe("Max results, default 10"),
|
|
36
|
+
},
|
|
37
|
+
}, async ({ query, trademark_class, live_only, limit }) => {
|
|
38
|
+
try {
|
|
39
|
+
const p = new URLSearchParams({ q: query, limit: String(limit ?? 10) });
|
|
40
|
+
if (trademark_class)
|
|
41
|
+
p.set("international_code", padClass(trademark_class));
|
|
42
|
+
if (live_only)
|
|
43
|
+
p.set("status_code", "800");
|
|
44
|
+
const data = await request(`/search?${p}`);
|
|
45
|
+
return ok({
|
|
46
|
+
total_matches: data.total_hits,
|
|
47
|
+
note: data.total_hits >= 1000
|
|
48
|
+
? "total_matches is capped at 1000; narrow with a class filter for a precise count"
|
|
49
|
+
: undefined,
|
|
50
|
+
results: data.hits.map(shapeMark),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return fail(err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.registerTool("check_trademark_availability", {
|
|
58
|
+
title: "Screen a proposed name against existing trademarks",
|
|
59
|
+
description: "Screen a proposed brand or company name against the US register and get existing marks ranked by similarity, each with a risk level. Use this BEFORE suggesting a company, product, or brand name to a user. This is a first-pass screening signal, not legal advice.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
name: z.string().describe("The proposed brand or company name to screen"),
|
|
62
|
+
trademark_class: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Nice class 1-45 for the goods/services. Strongly recommended — trademark rights are class-scoped, so screening without one produces noise."),
|
|
66
|
+
limit: z.number().int().min(1).max(25).optional().describe("Max conflicts to return, default 10"),
|
|
67
|
+
},
|
|
68
|
+
}, async ({ name, trademark_class, limit }) => {
|
|
69
|
+
try {
|
|
70
|
+
const p = new URLSearchParams({ mark: name, limit: String(limit ?? 10) });
|
|
71
|
+
if (trademark_class)
|
|
72
|
+
p.set("class", padClass(trademark_class));
|
|
73
|
+
const data = await request(`/clearance?${p}`);
|
|
74
|
+
return ok({
|
|
75
|
+
proposed_name: data.mark,
|
|
76
|
+
scoped_to_class: data.class ? String(parseInt(data.class, 10)) : null,
|
|
77
|
+
highest_risk: data.highest_risk,
|
|
78
|
+
conflicts: data.matches.map((m) => ({
|
|
79
|
+
...shapeMark(m),
|
|
80
|
+
similarity: m.similarity,
|
|
81
|
+
risk: m.risk,
|
|
82
|
+
})),
|
|
83
|
+
disclaimer: "Screening signal only. Likelihood of confusion is a legal judgment and this does not cover state or common-law rights. Recommend a trademark attorney before adopting a name.",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return fail(err);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
server.registerTool("get_trademark", {
|
|
91
|
+
title: "Get a trademark by serial number",
|
|
92
|
+
description: "Fetch the complete record for one US trademark by its USPTO serial number, including owner, status, classes, and goods/services.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
serial_number: z.string().describe("USPTO serial number, e.g. '72414177'"),
|
|
95
|
+
},
|
|
96
|
+
}, async ({ serial_number }) => {
|
|
97
|
+
try {
|
|
98
|
+
const data = await request(`/trademark/${encodeURIComponent(serial_number)}`);
|
|
99
|
+
return ok(shapeMark(data));
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return fail(err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.registerTool("get_trademark_timeline", {
|
|
106
|
+
title: "Get a trademark's prosecution history",
|
|
107
|
+
description: "Fetch the event timeline for a US trademark — filing, publication, office actions, registration, and any abandonment or cancellation.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
serial_number: z.string().describe("USPTO serial number, e.g. '72414177'"),
|
|
110
|
+
},
|
|
111
|
+
}, async ({ serial_number }) => {
|
|
112
|
+
try {
|
|
113
|
+
const data = await request(`/trademark/${encodeURIComponent(serial_number)}/timeline`);
|
|
114
|
+
return ok(data);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return fail(err);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
server.registerTool("search_by_owner", {
|
|
121
|
+
title: "List trademarks held by an owner",
|
|
122
|
+
description: "Find every US trademark held by a person or company. Use this to inspect a competitor's or a client's trademark portfolio.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
owner: z.string().describe("Owner name, e.g. 'Nike, Inc.'"),
|
|
125
|
+
limit: z.number().int().min(1).max(25).optional().describe("Max results, default 10"),
|
|
126
|
+
},
|
|
127
|
+
}, async ({ owner, limit }) => {
|
|
128
|
+
try {
|
|
129
|
+
const p = new URLSearchParams({ name: owner, limit: String(limit ?? 10) });
|
|
130
|
+
const data = await request(`/owner?${p}`);
|
|
131
|
+
return ok({ total_matches: data.total_hits, results: data.hits.map(shapeMark) });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return fail(err);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
server.registerTool("list_trademark_classes", {
|
|
138
|
+
title: "List the 45 trademark classes",
|
|
139
|
+
description: "List all 45 Nice Classification classes with their titles. Use this to pick the right class before screening a name — classes 1-34 are goods, 35-45 are services.",
|
|
140
|
+
inputSchema: {},
|
|
141
|
+
}, async () => {
|
|
142
|
+
try {
|
|
143
|
+
const data = await request("/classes");
|
|
144
|
+
return ok(data.classes.map((c) => ({
|
|
145
|
+
class: String(parseInt(c.code, 10)),
|
|
146
|
+
title: c.title,
|
|
147
|
+
kind: parseInt(c.code, 10) >= 35 ? "services" : "goods",
|
|
148
|
+
})));
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return fail(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const transport = new StdioServerTransport();
|
|
155
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radsoft/markbase-mcp",
|
|
3
|
+
"version": "2026.8.1",
|
|
4
|
+
"description": "MCP server for the Markbase USPTO trademark API \u2014 search, screen, and look up US trademarks from any MCP client.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"markbase-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"trademark",
|
|
17
|
+
"uspto",
|
|
18
|
+
"trademark-search",
|
|
19
|
+
"clearance",
|
|
20
|
+
"markbase"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://markbase.co",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Rad-Soft/marked-api.git",
|
|
26
|
+
"directory": "mcp"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"prepublishOnly": "bun run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
35
|
+
"zod": "^3.25.76"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.10.2",
|
|
39
|
+
"typescript": "^5.7.2"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|