@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 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
+ }