nectr-mcp 0.1.0 → 0.2.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 +36 -4
- package/dist/client.js +1 -9
- package/dist/server.js +1 -1
- package/dist/tools.js +0 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An [MCP](https://modelcontextprotocol.io) server for [nectr](https://nectr.ch), the markdown library for your LLMs.
|
|
4
4
|
|
|
5
|
-
It lets a model **search** your library, **read** any saved page as clean markdown, **save** new pages
|
|
5
|
+
It lets a model **search** your library, **read** any saved page as clean markdown, and **save** new pages. You don't need to paste links into the chat.
|
|
6
6
|
|
|
7
7
|
- **Search:** a model can't guess your permalinks, but it can call `search_library("pricing page")`.
|
|
8
8
|
- **Private pages:** pages you haven't shared have no working public link, but your key can still read them.
|
|
@@ -43,8 +43,7 @@ Node.js 20 or newer is required.
|
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| `search_library(query?, limit?)` | Finds saved pages by title or URL. Returns ids, URLs, freshness and a two-sentence summary per page, but **not** the bodies. Leave out `query` to list the most recently refreshed pages. | no |
|
|
45
45
|
| `get_page(id \| url, max_chars?)` | Returns the clean markdown of one page, with when it was last fetched. Long pages are cut off at 40,000 characters by default; ask for more with `max_chars`. | no |
|
|
46
|
-
| `convert_url(url,
|
|
47
|
-
| `list_projects()` | Lists your projects with page counts and each project's public `llms.txt` URL. | no |
|
|
46
|
+
| `convert_url(url, force_refresh?)` | Saves a web page, PDF or DOCX to your library. Counts against your monthly conversion quota unless the page is already saved. | yes |
|
|
48
47
|
|
|
49
48
|
There is deliberately **no delete**. A model that could delete your library by misreading a request isn't worth the convenience.
|
|
50
49
|
|
|
@@ -72,8 +71,10 @@ The server uses the same developer API as any other script, so your plan's limit
|
|
|
72
71
|
|
|
73
72
|
## Development
|
|
74
73
|
|
|
74
|
+
Running the server needs Node 20+, but **developing needs Node 22.12 or newer** (Vitest and the MCP Inspector require it). CI uses Node 24.
|
|
75
|
+
|
|
75
76
|
```bash
|
|
76
|
-
npm install
|
|
77
|
+
npm ci # not `npm install`: installs exactly the lockfile and never rewrites it
|
|
77
78
|
npm run typecheck
|
|
78
79
|
npm test
|
|
79
80
|
npm run build
|
|
@@ -82,6 +83,37 @@ NECTR_API_KEY=nct_... node dist/index.js # speaks MCP over stdio
|
|
|
82
83
|
|
|
83
84
|
The tests run against a fake nectr API (`test/fake-api.ts`) and through a real MCP client over an in-memory transport (`test/server.test.ts`).
|
|
84
85
|
|
|
86
|
+
If `npm test` fails with **"Cannot find native binding"**, the `node_modules` folder was installed on another platform, for example inside a Linux container. `npm install` on top of it won't fix it. Delete `node_modules` and run `npm ci` again.
|
|
87
|
+
|
|
88
|
+
### Trying it by hand with the MCP Inspector
|
|
89
|
+
|
|
90
|
+
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) calls the tools directly, without a model, and shows exactly what a model would receive. Build first (`npm run build`), then use its CLI mode. **Put the server command before the options.** With the options first, the Inspector reports "No servers found in config file".
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
# PowerShell; in bash, use $NECTR_API_KEY instead of $k
|
|
94
|
+
$k = "nct_your_key"
|
|
95
|
+
|
|
96
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js -e NECTR_API_KEY=$k --method tools/list
|
|
97
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js -e NECTR_API_KEY=$k --method tools/call --tool-name search_library
|
|
98
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js -e NECTR_API_KEY=$k --method tools/call --tool-name search_library --tool-arg query=pricing
|
|
99
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js -e NECTR_API_KEY=$k --method tools/call --tool-name get_page --tool-arg id=PAGE-ID
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Add `-e NECTR_URL=http://localhost:8000` to test against a local nectr backend. Leave out `--cli` for the web interface; the argument order is the same.
|
|
103
|
+
|
|
104
|
+
The Inspector's own dependencies need a newer Node than this server does. If it fails with **"Cannot find native binding"** after you upgrade Node, delete its cached copy from the npx cache (`%LocalAppData%\npm-cache\_npx` on Windows, `~/.npm/_npx` elsewhere) and run it again.
|
|
105
|
+
|
|
106
|
+
The quickest sanity check needs no key at all: `npx -y nectr-mcp` should print "NECTR_API_KEY is not set" and exit.
|
|
107
|
+
|
|
108
|
+
### Publishing
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm version patch # or minor / major; also bump VERSION in src/server.ts and USER_AGENT in src/client.ts
|
|
112
|
+
npm publish # prepublishOnly runs typecheck, tests and build first
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
npm refuses to publish from an account without two-factor authentication (error `E403`). Enable it under your npm account settings; `npm publish` then asks you to confirm in the browser or with `--otp=<code>`.
|
|
116
|
+
|
|
85
117
|
## License
|
|
86
118
|
|
|
87
119
|
MIT
|
package/dist/client.js
CHANGED
|
@@ -25,7 +25,7 @@ export class NectrUnreachableError extends Error {
|
|
|
25
25
|
this.name = "NectrUnreachableError";
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
const USER_AGENT = "nectr-mcp/0.
|
|
28
|
+
const USER_AGENT = "nectr-mcp/0.2.0";
|
|
29
29
|
const TIMEOUT_MS = 60_000; // a real crawl can take a while; reads are fast
|
|
30
30
|
export class NectrClient {
|
|
31
31
|
apiKey;
|
|
@@ -50,14 +50,6 @@ export class NectrClient {
|
|
|
50
50
|
convert(url, forceRefresh) {
|
|
51
51
|
return this.request("POST", "/conversions", { url, force_refresh: forceRefresh });
|
|
52
52
|
}
|
|
53
|
-
assignProject(id, projectId) {
|
|
54
|
-
return this.request("PATCH", `/conversions/${encodeURIComponent(id)}/project`, {
|
|
55
|
-
project_id: projectId,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
listProjects() {
|
|
59
|
-
return this.request("GET", "/projects");
|
|
60
|
-
}
|
|
61
53
|
me() {
|
|
62
54
|
return this.request("GET", "/auth/me");
|
|
63
55
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { registerTools } from "./tools.js";
|
|
3
|
-
export const VERSION = "0.
|
|
3
|
+
export const VERSION = "0.2.0";
|
|
4
4
|
export function createServer(client) {
|
|
5
5
|
const server = new McpServer({ name: "nectr", version: VERSION });
|
|
6
6
|
registerTools(server, client);
|
package/dist/tools.js
CHANGED
|
@@ -98,43 +98,16 @@ export async function getPage(client, args) {
|
|
|
98
98
|
export async function convertUrl(client, args) {
|
|
99
99
|
try {
|
|
100
100
|
const saved = await client.convert(args.url, args.force_refresh ?? false);
|
|
101
|
-
let projectLine = "";
|
|
102
|
-
if (args.project_id) {
|
|
103
|
-
try {
|
|
104
|
-
await client.assignProject(saved.id, args.project_id);
|
|
105
|
-
projectLine = `\nAdded to project ${args.project_id}.`;
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
// The page IS saved; say so rather than reporting the whole call as failed.
|
|
109
|
-
projectLine = `\nSaved, but could not add it to project ${args.project_id}: ${describeError(err)}`;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
101
|
const how = saved.cached ? "Already in the library (served the saved copy)" : "Saved";
|
|
113
102
|
return text(`${how}: ${titleOf(saved)}\n` +
|
|
114
103
|
`id: ${saved.id} · ${saved.word_count} words · fetched: ${saved.fetched_at}` +
|
|
115
104
|
(saved.summary ? `\nsummary: ${saved.summary}` : "") +
|
|
116
|
-
projectLine +
|
|
117
105
|
`\n\nnectr keeps this page current on the plan's refresh schedule. Read it with get_page("${saved.id}").`);
|
|
118
106
|
}
|
|
119
107
|
catch (err) {
|
|
120
108
|
return failure(err);
|
|
121
109
|
}
|
|
122
110
|
}
|
|
123
|
-
export async function listProjects(client) {
|
|
124
|
-
try {
|
|
125
|
-
const [projects, me] = await Promise.all([client.listProjects(), client.me()]);
|
|
126
|
-
if (projects.length === 0)
|
|
127
|
-
return text("No projects yet. Projects are created in the nectr dashboard.");
|
|
128
|
-
const rows = projects.map((p) => `- ${p.name} (${plural(p.conversion_count, "page")})\n` +
|
|
129
|
-
` id: ${p.id}\n` +
|
|
130
|
-
` llms.txt: ${client.siteUrl}/p/${me.link_hash}/${p.slug}/llms.txt`);
|
|
131
|
-
return text(`${plural(projects.length, "project")}:\n\n${rows.join("\n")}\n\n` +
|
|
132
|
-
"Each llms.txt is public and lists only the project's shareable pages.");
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
return failure(err);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
111
|
// ── Registration ────────────────────────────────────────────────────────────
|
|
139
112
|
export function registerTools(server, client) {
|
|
140
113
|
server.registerTool("search_library", {
|
|
@@ -170,7 +143,6 @@ export function registerTools(server, client) {
|
|
|
170
143
|
"Counts against the user's monthly conversion quota unless the page is already saved.",
|
|
171
144
|
inputSchema: {
|
|
172
145
|
url: z.url().describe("The http(s) URL to save."),
|
|
173
|
-
project_id: z.string().optional().describe("Optional project id (from list_projects) to file it under."),
|
|
174
146
|
force_refresh: z
|
|
175
147
|
.boolean()
|
|
176
148
|
.optional()
|
|
@@ -178,10 +150,4 @@ export function registerTools(server, client) {
|
|
|
178
150
|
},
|
|
179
151
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
180
152
|
}, (args) => convertUrl(client, args));
|
|
181
|
-
server.registerTool("list_projects", {
|
|
182
|
-
title: "List nectr projects",
|
|
183
|
-
description: "List the user's nectr projects with their page counts and public llms.txt URLs.",
|
|
184
|
-
inputSchema: {},
|
|
185
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
186
|
-
}, () => listProjects(client));
|
|
187
153
|
}
|