@patentprecheck/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/SKILL.md +44 -0
- package/bin/precheck.js +145 -0
- package/package.json +42 -0
- package/src/api.js +98 -0
- package/src/render.js +112 -0
- package/src/server.js +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @patentprecheck/mcp — Patent PreCheck CLI + MCP server
|
|
2
|
+
|
|
3
|
+
Run a patentability pre-check on your code from inside your terminal or your AI
|
|
4
|
+
coding agent (Cursor, Claude Code, Codex, Antigravity). It wraps the hosted
|
|
5
|
+
[Patent PreCheck](https://patentprecheck.com) engine, so **no API keys are
|
|
6
|
+
required** — the scoring engine and prior-art corpus stay server-side.
|
|
7
|
+
|
|
8
|
+
> Informational only — not legal advice and not a substitute for a licensed patent attorney.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
- Scores code or an invention description across the four USPTO statutory pillars
|
|
13
|
+
(§101 eligibility, §102 novelty, §103 non-obviousness, §101 utility) plus a
|
|
14
|
+
separate §112 filing-readiness signal.
|
|
15
|
+
- Reports the band (Not Ready → File Ready), the pillar holding the band back,
|
|
16
|
+
top opportunities to strengthen, and how much prior art was consulted.
|
|
17
|
+
- Exposes MCP tools so an agent can score the code it just wrote, inline.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
# Run directly (no install)
|
|
23
|
+
npx -y @patentprecheck/mcp score ./src/widget.ts
|
|
24
|
+
|
|
25
|
+
# Or install globally
|
|
26
|
+
npm i -g @patentprecheck/mcp
|
|
27
|
+
precheck score ./src/widget.ts
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## CLI
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
precheck score ./path/to/file.ts # human-readable report
|
|
34
|
+
precheck score ./file.ts --format json # raw JSON
|
|
35
|
+
cat invention.md | precheck score - # read from stdin
|
|
36
|
+
precheck score ./file.ts --min-score 60 # exit 4 if below threshold (CI gate)
|
|
37
|
+
precheck pillars # scoring reference (no network)
|
|
38
|
+
precheck review # Interactive Code Review signup URL
|
|
39
|
+
precheck mcp # run as an MCP server over stdio
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Exit codes: `0` ok · `1` usage · `2` request error · `3` §101 gate not passed ·
|
|
43
|
+
`4` below `--min-score`. stdout is clean data; progress/errors go to stderr.
|
|
44
|
+
|
|
45
|
+
## Use in an AI coding agent
|
|
46
|
+
|
|
47
|
+
See [`config-examples/`](./config-examples/). Quickest paths:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
# Claude Code
|
|
51
|
+
claude mcp add patent-precheck -- npx -y @patentprecheck/mcp mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
// Cursor — .cursor/mcp.json
|
|
56
|
+
{ "mcpServers": { "patent-precheck": { "command": "npx", "args": ["-y", "@patentprecheck/mcp", "mcp"] } } }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
MCP tools: `precheck_score`, `precheck_pillars`, `precheck_start_review`.
|
|
60
|
+
|
|
61
|
+
## Develop from source
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
cd tools/precheck-mcp
|
|
65
|
+
npm install
|
|
66
|
+
node bin/precheck.js pillars
|
|
67
|
+
echo "a novel rate limiter that ..." | node bin/precheck.js score - --format text
|
|
68
|
+
node bin/precheck.js mcp # stdio server; blocks waiting for an MCP client
|
|
69
|
+
```
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patent-precheck
|
|
3
|
+
description: Score code for patentability with Patent PreCheck. Use when the user asks "can this be patented?", wants a patentability/novelty/§101/§102/§103 check, or wants to know if an invention or codebase is filing-ready.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Patent PreCheck — AI agent usage guide
|
|
7
|
+
|
|
8
|
+
`precheck` scores source code or an invention description against the four USPTO
|
|
9
|
+
statutory patentability pillars and reports filing readiness. It calls the hosted
|
|
10
|
+
Patent PreCheck engine over HTTP — no local keys required.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- The user asks whether code/an invention "can be patented", is "novel", "non-obvious",
|
|
15
|
+
"eligible (§101)", or "ready to file (§112)".
|
|
16
|
+
- The user wants a quick patentability score before talking to an attorney.
|
|
17
|
+
|
|
18
|
+
## MCP tools
|
|
19
|
+
|
|
20
|
+
| Tool | Purpose |
|
|
21
|
+
|------|---------|
|
|
22
|
+
| `precheck_score` | Score `code` (or a local `path`) for patentability. Returns score, band, per-pillar scores, the limiting pillar, top opportunities, and prior-art count. |
|
|
23
|
+
| `precheck_pillars` | Reference: the five pillars (statutes + weights) and band rules. Use to explain a score. No network. |
|
|
24
|
+
| `precheck_start_review` | URL to start a paid Interactive Code Review (evidence + filing package). |
|
|
25
|
+
|
|
26
|
+
## How to call `precheck_score`
|
|
27
|
+
|
|
28
|
+
- Pass the relevant code as `code`, or a file path as `path`. Prefer the specific
|
|
29
|
+
module/function the user is asking about over an entire repo — focused input scores better.
|
|
30
|
+
- Optional `filename` improves language/context detection.
|
|
31
|
+
- Default `tier` is `free`; one free score per invention. A `402`/upgrade error means
|
|
32
|
+
the free analysis was used — offer `precheck_start_review`.
|
|
33
|
+
|
|
34
|
+
## Interpreting results
|
|
35
|
+
|
|
36
|
+
- `band` runs Not Ready → Building → Close to Ready → File Ready.
|
|
37
|
+
- Bands enforce **per-pillar floors**, not just the weighted average: a composite of 82
|
|
38
|
+
with one pillar at 55 is **not** File Ready. `band_held_back_by` names the limiting pillar.
|
|
39
|
+
- `patentability_score` (the four pillars) is primary; `filing_readiness_score` (§112) is a
|
|
40
|
+
separate documentation-quality signal.
|
|
41
|
+
- If `gate_passed` is false, the subject matter is not §101-eligible and no score is issued.
|
|
42
|
+
|
|
43
|
+
Always explain the limiting pillar and one or two concrete `top_opportunities`, and remind
|
|
44
|
+
the user this is an informational tool, not legal advice.
|
package/bin/precheck.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Patent PreCheck CLI.
|
|
3
|
+
//
|
|
4
|
+
// precheck score <file|-> Score a file (or stdin) for patentability
|
|
5
|
+
// precheck pillars Print the scoring reference (pillars + bands)
|
|
6
|
+
// precheck review Print the Interactive Code Review signup URL
|
|
7
|
+
// precheck mcp Run as an MCP server over stdio (for AI agents)
|
|
8
|
+
//
|
|
9
|
+
// stdout carries data; progress/errors go to stderr; exit codes are typed so
|
|
10
|
+
// the command composes in scripts and CI.
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
import { callAnalyze, reviewSignupUrl } from '../src/api.js';
|
|
17
|
+
import { renderScoreText, renderPillarsReference } from '../src/render.js';
|
|
18
|
+
import { runStdio } from '../src/server.js';
|
|
19
|
+
|
|
20
|
+
const EXIT_OK = 0;
|
|
21
|
+
const EXIT_USAGE = 1;
|
|
22
|
+
const EXIT_REQUEST = 2;
|
|
23
|
+
const EXIT_GATE = 3;
|
|
24
|
+
const EXIT_BELOW_MIN = 4;
|
|
25
|
+
|
|
26
|
+
function version() {
|
|
27
|
+
try {
|
|
28
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
return JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8')).version;
|
|
30
|
+
} catch {
|
|
31
|
+
return '0.0.0';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`precheck \u2014 Patent PreCheck CLI + MCP server (v${version()})\n\n` +
|
|
38
|
+
'Usage:\n' +
|
|
39
|
+
' precheck score <file|-> [--filename name.ext] [--tier free] [--format text|json] [--min-score N]\n' +
|
|
40
|
+
' precheck pillars\n' +
|
|
41
|
+
' precheck review\n' +
|
|
42
|
+
' precheck mcp\n\n' +
|
|
43
|
+
'Env: PRECHECK_API_URL, PRECHECK_TIER, PRECHECK_SITE_URL, PRECHECK_AI_ASSISTANCE\n',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseScoreArgs(argv) {
|
|
48
|
+
const opts = { input: null, filename: null, tier: undefined, format: 'text', minScore: null };
|
|
49
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
50
|
+
const a = argv[i];
|
|
51
|
+
if (a === '--filename') opts.filename = argv[++i];
|
|
52
|
+
else if (a === '--tier') opts.tier = argv[++i];
|
|
53
|
+
else if (a === '--format') opts.format = argv[++i];
|
|
54
|
+
else if (a === '--min-score') opts.minScore = Number(argv[++i]);
|
|
55
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
56
|
+
else if (!a.startsWith('-') || a === '-') opts.input = a;
|
|
57
|
+
}
|
|
58
|
+
return opts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readStdin() {
|
|
62
|
+
return fs.readFileSync(0, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function cmdScore(argv) {
|
|
66
|
+
const opts = parseScoreArgs(argv);
|
|
67
|
+
if (opts.help || !opts.input) {
|
|
68
|
+
usage();
|
|
69
|
+
process.exit(opts.help ? EXIT_OK : EXIT_USAGE);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let code;
|
|
73
|
+
let filename = opts.filename;
|
|
74
|
+
if (opts.input === '-') {
|
|
75
|
+
code = readStdin();
|
|
76
|
+
if (!filename) filename = 'stdin.txt';
|
|
77
|
+
} else {
|
|
78
|
+
const filePath = path.resolve(opts.input);
|
|
79
|
+
if (!fs.existsSync(filePath)) {
|
|
80
|
+
process.stderr.write(`File not found: ${filePath}\n`);
|
|
81
|
+
process.exit(EXIT_USAGE);
|
|
82
|
+
}
|
|
83
|
+
code = fs.readFileSync(filePath, 'utf8');
|
|
84
|
+
if (!filename) filename = path.basename(filePath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.stderr.write('Analyzing with Patent PreCheck\u2026\n');
|
|
88
|
+
const { ok, data, error } = await callAnalyze({ code, filename, tier: opts.tier });
|
|
89
|
+
if (!ok) {
|
|
90
|
+
process.stderr.write(`${error}\n`);
|
|
91
|
+
process.exit(EXIT_REQUEST);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (opts.format === 'json') {
|
|
95
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
96
|
+
} else {
|
|
97
|
+
process.stdout.write(renderScoreText(data, { filename }) + '\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (data.gate_passed === false) process.exit(EXIT_GATE);
|
|
101
|
+
|
|
102
|
+
const score = Number(data.patentability_score ?? data.overall_score);
|
|
103
|
+
if (Number.isFinite(opts.minScore) && Number.isFinite(score) && score < opts.minScore) {
|
|
104
|
+
process.stderr.write(`Patentability score ${score} is below --min-score ${opts.minScore}\n`);
|
|
105
|
+
process.exit(EXIT_BELOW_MIN);
|
|
106
|
+
}
|
|
107
|
+
process.exit(EXIT_OK);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const [, , cmd, ...rest] = process.argv;
|
|
112
|
+
switch (cmd) {
|
|
113
|
+
case 'score':
|
|
114
|
+
await cmdScore(rest);
|
|
115
|
+
break;
|
|
116
|
+
case 'pillars':
|
|
117
|
+
process.stdout.write(renderPillarsReference() + '\n');
|
|
118
|
+
break;
|
|
119
|
+
case 'review':
|
|
120
|
+
process.stdout.write(reviewSignupUrl() + '\n');
|
|
121
|
+
break;
|
|
122
|
+
case 'mcp':
|
|
123
|
+
await runStdio();
|
|
124
|
+
break;
|
|
125
|
+
case '--version':
|
|
126
|
+
case '-V':
|
|
127
|
+
process.stdout.write(version() + '\n');
|
|
128
|
+
break;
|
|
129
|
+
case '--help':
|
|
130
|
+
case '-h':
|
|
131
|
+
case undefined:
|
|
132
|
+
usage();
|
|
133
|
+
process.exit(cmd === undefined ? EXIT_USAGE : EXIT_OK);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
process.stderr.write(`Unknown command: ${cmd}\n`);
|
|
137
|
+
usage();
|
|
138
|
+
process.exit(EXIT_USAGE);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch((err) => {
|
|
143
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
144
|
+
process.exit(EXIT_REQUEST);
|
|
145
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patentprecheck/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Patent PreCheck CLI + MCP server — run a patentability pre-check on your code from inside Cursor, Claude Code, Codex, and other AI coding agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"precheck": "bin/precheck.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/server.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"SKILL.md",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "node bin/precheck.js mcp",
|
|
23
|
+
"smoke": "node bin/precheck.js score - --format text"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"patent",
|
|
27
|
+
"patentability",
|
|
28
|
+
"mcp",
|
|
29
|
+
"cli",
|
|
30
|
+
"ai-agent",
|
|
31
|
+
"cursor",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"codex",
|
|
34
|
+
"prior-art"
|
|
35
|
+
],
|
|
36
|
+
"author": "Patent PreCheck",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
40
|
+
"zod": "^3.23.8"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Thin client for the public Patent PreCheck analyze API.
|
|
2
|
+
//
|
|
3
|
+
// The MCP server / CLI never talks to the database or any LLM directly — it
|
|
4
|
+
// POSTs to the hosted endpoint, so an installing developer needs no API keys
|
|
5
|
+
// or secrets. The scoring engine, prior-art corpus, and source registry all
|
|
6
|
+
// stay server-side.
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API = 'https://patentprecheck.com/.netlify/functions/analyze';
|
|
9
|
+
const DEFAULT_SITE = 'https://patentprecheck.com';
|
|
10
|
+
|
|
11
|
+
// Mirror the env conventions already used by scripts/precheck-from-file.js so
|
|
12
|
+
// the CLI and MCP server behave identically. PRECHECK_* is preferred; the
|
|
13
|
+
// legacy PPC_* names are accepted as fallbacks.
|
|
14
|
+
export function apiUrl() {
|
|
15
|
+
return process.env.PRECHECK_API_URL || process.env.PPC_API_URL || DEFAULT_API;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function siteUrl() {
|
|
19
|
+
return process.env.PRECHECK_SITE_URL || process.env.PPC_SITE_URL || DEFAULT_SITE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defaultTier() {
|
|
23
|
+
return process.env.PRECHECK_TIER || process.env.PPC_TIER || 'free';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function defaultAiAssistance() {
|
|
27
|
+
return process.env.PRECHECK_AI_ASSISTANCE || process.env.PPC_AI_ASSISTANCE || 'yes_some';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const MIN_CODE_CHARS = 10;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Call the hosted analyze endpoint.
|
|
34
|
+
* @returns {Promise<{ok: boolean, status: number, data: any, error: string|null}>}
|
|
35
|
+
*/
|
|
36
|
+
export async function callAnalyze({ code, filename, tier, aiAssistance, timeoutMs = 60000 } = {}) {
|
|
37
|
+
if (typeof code !== 'string' || code.trim().length < MIN_CODE_CHARS) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
status: 0,
|
|
41
|
+
data: null,
|
|
42
|
+
error: `Provide at least ${MIN_CODE_CHARS} characters of code or an invention description.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const body = {
|
|
47
|
+
code,
|
|
48
|
+
filename: filename || 'invention.txt',
|
|
49
|
+
tier: tier || defaultTier(),
|
|
50
|
+
ai_assistance_declared: aiAssistance || defaultAiAssistance(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
55
|
+
let res;
|
|
56
|
+
try {
|
|
57
|
+
res = await fetch(apiUrl(), {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
const msg = err && err.name === 'AbortError' ? 'analyze request timed out' : err.message;
|
|
66
|
+
return { ok: false, status: 0, data: null, error: `request failed: ${msg}` };
|
|
67
|
+
}
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
data = JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: res.status,
|
|
78
|
+
data: null,
|
|
79
|
+
error: `non-JSON response (${res.status}): ${text.slice(0, 300)}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
status: res.status,
|
|
87
|
+
data,
|
|
88
|
+
error: data.error || `analyze failed (HTTP ${res.status})`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { ok: true, status: res.status, data, error: null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** URL that starts a paid Interactive Code Review for a given invention. */
|
|
96
|
+
export function reviewSignupUrl() {
|
|
97
|
+
return `${siteUrl()}/review-signup`;
|
|
98
|
+
}
|
package/src/render.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Human-readable rendering of analyze results, shared by the CLI and the MCP
|
|
2
|
+
// server so an agent and a terminal user see the same summary.
|
|
3
|
+
|
|
4
|
+
import { reviewSignupUrl } from './api.js';
|
|
5
|
+
|
|
6
|
+
export const PILLARS = [
|
|
7
|
+
{ key: 'eligibility', label: 'Eligibility', statute: '§101 (Alice/Mayo)', weight: '25%', question: 'Is it more than an abstract idea?' },
|
|
8
|
+
{ key: 'novelty', label: 'Novelty', statute: '§102', weight: '25%', question: 'Is it new?' },
|
|
9
|
+
{ key: 'non_obvious', label: 'Non-obviousness', statute: '§103', weight: '30%', question: 'Is it inventive?' },
|
|
10
|
+
{ key: 'utility', label: 'Utility', statute: '§101 (utility)', weight: '10%', question: 'Does it work and does it matter?' },
|
|
11
|
+
{ key: 'filing_readiness', label: 'Filing readiness', statute: '§112', weight: '10%', question: 'Is your documentation strong enough to file?' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const BANDS = [
|
|
15
|
+
{ band: 'file_ready', label: 'File Ready', score: '\u2265 80', floor: 'every pillar \u2265 70' },
|
|
16
|
+
{ band: 'close_to_ready', label: 'Close to Ready', score: '\u2265 60', floor: 'every pillar \u2265 50' },
|
|
17
|
+
{ band: 'building', label: 'Building', score: '\u2265 40', floor: '\u2014' },
|
|
18
|
+
{ band: 'not_ready', label: 'Not Ready', score: '0\u201339', floor: '\u2014' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function humanizePillar(key) {
|
|
22
|
+
const found = PILLARS.find((p) => p.key === key);
|
|
23
|
+
return found ? `${found.label} (${found.statute})` : key || '\u2014';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Render the pillars/bands reference (no network). */
|
|
27
|
+
export function renderPillarsReference() {
|
|
28
|
+
const lines = [];
|
|
29
|
+
lines.push('Patent PreCheck scores four statutory patentability pillars (weighted) plus a');
|
|
30
|
+
lines.push('separate Filing Readiness (\u00a7112) signal, behind a \u00a7101 subject-matter gate.');
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push('Pillars:');
|
|
33
|
+
for (const p of PILLARS) {
|
|
34
|
+
lines.push(` ${p.label.padEnd(18)} ${p.statute.padEnd(18)} ${p.weight.padEnd(5)} ${p.question}`);
|
|
35
|
+
}
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push('Bands (enforced floors, not just a weighted average):');
|
|
38
|
+
for (const b of BANDS) {
|
|
39
|
+
lines.push(` ${b.label.padEnd(16)} score ${b.score.padEnd(6)} floor: ${b.floor}`);
|
|
40
|
+
}
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push('A composite of 82 with one pillar at 55 does NOT reach File Ready; band_held_back_by');
|
|
43
|
+
lines.push('names the limiting pillar so you know exactly what to strengthen.');
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render an analyze result as a compact, terminal-friendly report. */
|
|
48
|
+
export function renderScoreText(data, { filename } = {}) {
|
|
49
|
+
if (!data || typeof data !== 'object') return 'No result.';
|
|
50
|
+
|
|
51
|
+
const lines = [];
|
|
52
|
+
const title = filename ? `Patent PreCheck \u2014 ${filename}` : 'Patent PreCheck';
|
|
53
|
+
lines.push(title);
|
|
54
|
+
lines.push('='.repeat(title.length));
|
|
55
|
+
|
|
56
|
+
if (data.gate_passed === false) {
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('Gate: NOT eligible patentable subject matter (\u00a7101).');
|
|
59
|
+
if (data.gate_reason) lines.push(`Reason: ${data.gate_reason}`);
|
|
60
|
+
lines.push('No patentability score is issued for non-eligible subject matter.');
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const score = data.patentability_score ?? data.overall_score;
|
|
65
|
+
const bandLabel = data.patentability_band_label || data.band_label || data.band || '\u2014';
|
|
66
|
+
const heldBack = data.patentability_held_back_by || data.band_held_back_by;
|
|
67
|
+
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(`Band: ${bandLabel} (patentability ${score ?? '\u2014'}/100)`);
|
|
70
|
+
if (data.filing_readiness_score != null) {
|
|
71
|
+
lines.push(`Filing readiness (\u00a7112): ${data.filing_readiness_score}/100 ${data.filing_readiness_band_label || ''}`.trimEnd());
|
|
72
|
+
}
|
|
73
|
+
if (heldBack) lines.push(`Held back by: ${humanizePillar(heldBack)}`);
|
|
74
|
+
if (data.technology_domain) lines.push(`Domain: ${data.technology_domain}`);
|
|
75
|
+
|
|
76
|
+
const ps = data.pillar_scores || {};
|
|
77
|
+
if (Object.keys(ps).length) {
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push('Pillars:');
|
|
80
|
+
for (const p of PILLARS) {
|
|
81
|
+
if (ps[p.key] == null) continue;
|
|
82
|
+
const tag = p.key === 'filing_readiness' ? ' (secondary)' : '';
|
|
83
|
+
lines.push(` ${(`${p.label} (${p.statute})`).padEnd(34)} ${String(ps[p.key]).padStart(3)}${tag}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const opps = Array.isArray(data.top_opportunities) ? data.top_opportunities : [];
|
|
88
|
+
if (opps.length) {
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push('Top opportunities to strengthen:');
|
|
91
|
+
opps.slice(0, 5).forEach((o, i) => {
|
|
92
|
+
const area = o.area || humanizePillar(o.pillar);
|
|
93
|
+
lines.push(` ${i + 1}. ${area}${o.action ? ` \u2014 ${o.action}` : ''}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (data.prior_art_match_count != null) {
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(`Prior art consulted: ${data.prior_art_match_count}`);
|
|
100
|
+
const teaser = Array.isArray(data.prior_art_teaser) ? data.prior_art_teaser : [];
|
|
101
|
+
teaser.slice(0, 5).forEach((t) => {
|
|
102
|
+
const label = typeof t === 'string' ? t : t.title || t.publication_number || '';
|
|
103
|
+
if (label) lines.push(` \u2022 ${label}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(`Next step \u2014 strengthen this with a live, evidence-backed Interactive Code Review:`);
|
|
109
|
+
lines.push(` ${reviewSignupUrl()}`);
|
|
110
|
+
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Patent PreCheck MCP server.
|
|
2
|
+
//
|
|
3
|
+
// Exposes the hosted patentability engine as MCP tools so a developer inside
|
|
4
|
+
// Cursor / Claude Code / Codex can score the code they just wrote without
|
|
5
|
+
// leaving their agent. Transport is stdio; launch via `precheck mcp`.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
import { callAnalyze, reviewSignupUrl, MIN_CODE_CHARS } from './api.js';
|
|
17
|
+
import { renderScoreText, renderPillarsReference } from './render.js';
|
|
18
|
+
|
|
19
|
+
function readPkgVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8'));
|
|
23
|
+
return pkg.version || '0.0.0';
|
|
24
|
+
} catch {
|
|
25
|
+
return '0.0.0';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function textResult(text, { isError = false } = {}) {
|
|
30
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Build and return the configured MCP server. */
|
|
34
|
+
export function buildServer(version = readPkgVersion()) {
|
|
35
|
+
const server = new McpServer({ name: 'patent-precheck', version });
|
|
36
|
+
|
|
37
|
+
server.registerTool(
|
|
38
|
+
'precheck_score',
|
|
39
|
+
{
|
|
40
|
+
title: 'Patent PreCheck — score patentability',
|
|
41
|
+
description:
|
|
42
|
+
'Run a patentability pre-check on source code or an invention description. ' +
|
|
43
|
+
'Returns a 0\u2013100 patentability score across the four USPTO statutory pillars ' +
|
|
44
|
+
'(\u00a7101 eligibility, \u00a7102 novelty, \u00a7103 non-obviousness, \u00a7101 utility), a ' +
|
|
45
|
+
'separate \u00a7112 filing-readiness signal, the band (Not Ready \u2192 File Ready), the ' +
|
|
46
|
+
'pillar that holds the band back, top opportunities to strengthen, and a count of ' +
|
|
47
|
+
'prior-art matches consulted. Provide either `code` (the text) or `path` (a local file to read).',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
code: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('The source code or invention description to analyze (>= 10 chars).'),
|
|
53
|
+
path: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Path to a local file to read and analyze instead of passing `code` inline.'),
|
|
57
|
+
filename: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Optional filename hint (e.g. main.ts) used for language/context.'),
|
|
61
|
+
tier: z
|
|
62
|
+
.enum(['free', 'paid_review', 'enterprise'])
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('Analysis tier. Defaults to free; paid tiers require server-side entitlement.'),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
async ({ code, path: filePath, filename, tier }) => {
|
|
68
|
+
let text = typeof code === 'string' ? code : '';
|
|
69
|
+
let name = filename;
|
|
70
|
+
if (!text && filePath) {
|
|
71
|
+
try {
|
|
72
|
+
text = await readFile(filePath, 'utf8');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return textResult(`Could not read file "${filePath}": ${err.message}`, { isError: true });
|
|
75
|
+
}
|
|
76
|
+
if (!name) name = path.basename(filePath);
|
|
77
|
+
}
|
|
78
|
+
if (!text || text.trim().length < MIN_CODE_CHARS) {
|
|
79
|
+
return textResult(
|
|
80
|
+
`Provide at least ${MIN_CODE_CHARS} characters via "code" or a readable "path".`,
|
|
81
|
+
{ isError: true },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { ok, data, error } = await callAnalyze({ code: text, filename: name, tier });
|
|
86
|
+
if (!ok) {
|
|
87
|
+
const hint =
|
|
88
|
+
error && /HTTP 402|Upgrade/i.test(error)
|
|
89
|
+
? ' (this invention has used its free analysis; start an Interactive Code Review at ' +
|
|
90
|
+
reviewSignupUrl() +
|
|
91
|
+
')'
|
|
92
|
+
: '';
|
|
93
|
+
return textResult(`Patent PreCheck error: ${error}${hint}`, { isError: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const summary = renderScoreText(data, { filename: name });
|
|
97
|
+
const compact = JSON.stringify(data);
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{ type: 'text', text: summary },
|
|
101
|
+
{ type: 'text', text: `Raw result JSON:\n\`\`\`json\n${compact}\n\`\`\`` },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
server.registerTool(
|
|
108
|
+
'precheck_pillars',
|
|
109
|
+
{
|
|
110
|
+
title: 'Patent PreCheck — scoring reference',
|
|
111
|
+
description:
|
|
112
|
+
'List the five patentability pillars (with statutes and weights) and the band rules ' +
|
|
113
|
+
'used by precheck_score. Use this to explain a score to the user. No network call.',
|
|
114
|
+
inputSchema: {},
|
|
115
|
+
},
|
|
116
|
+
async () => textResult(renderPillarsReference()),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
server.registerTool(
|
|
120
|
+
'precheck_start_review',
|
|
121
|
+
{
|
|
122
|
+
title: 'Patent PreCheck — start an Interactive Code Review',
|
|
123
|
+
description:
|
|
124
|
+
'Return the URL where the user can start a paid, live Interactive Code Review that ' +
|
|
125
|
+
'strengthens each pillar with evidence and produces a filing package. Use after a ' +
|
|
126
|
+
'precheck_score when the user wants to act on the result.',
|
|
127
|
+
inputSchema: {},
|
|
128
|
+
},
|
|
129
|
+
async () =>
|
|
130
|
+
textResult(
|
|
131
|
+
`Start an Interactive Code Review (live coaching + evidence + filing package):\n${reviewSignupUrl()}`,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return server;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Connect the server over stdio and block until the client disconnects. */
|
|
139
|
+
export async function runStdio() {
|
|
140
|
+
const server = buildServer();
|
|
141
|
+
const transport = new StdioServerTransport();
|
|
142
|
+
await server.connect(transport);
|
|
143
|
+
}
|