@qianfuv/paper-scanner-mcp 1.0.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/dist/client.d.ts +32 -0
- package/dist/client.js +102 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/articles.d.ts +4 -0
- package/dist/tools/articles.js +51 -0
- package/dist/tools/articles.js.map +1 -0
- package/dist/tools/favorites.d.ts +4 -0
- package/dist/tools/favorites.js +51 -0
- package/dist/tools/favorites.js.map +1 -0
- package/dist/tools/journals.d.ts +4 -0
- package/dist/tools/journals.js +24 -0
- package/dist/tools/journals.js.map +1 -0
- package/dist/tools/meta.d.ts +4 -0
- package/dist/tools/meta.js +34 -0
- package/dist/tools/meta.js.map +1 -0
- package/dist/tools/weekly.d.ts +4 -0
- package/dist/tools/weekly.js +19 -0
- package/dist/tools/weekly.js.map +1 -0
- package/package.json +29 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type Primitive = boolean | number | string;
|
|
2
|
+
type QueryValue = Primitive | Primitive[] | null | undefined;
|
|
3
|
+
type QueryParams = Record<string, QueryValue>;
|
|
4
|
+
type RequestOptions = {
|
|
5
|
+
auth?: boolean;
|
|
6
|
+
body?: unknown;
|
|
7
|
+
db?: string;
|
|
8
|
+
query?: QueryParams;
|
|
9
|
+
};
|
|
10
|
+
declare class PaperScannerClient {
|
|
11
|
+
private readonly apiToken;
|
|
12
|
+
private readonly baseUrl;
|
|
13
|
+
private readonly defaultDb;
|
|
14
|
+
constructor();
|
|
15
|
+
getDefaultDb(): string | null;
|
|
16
|
+
delete<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
17
|
+
get<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
18
|
+
post<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
19
|
+
private appendQueryParams;
|
|
20
|
+
private buildUrl;
|
|
21
|
+
private normalizeBaseUrl;
|
|
22
|
+
private readOptionalEnv;
|
|
23
|
+
private request;
|
|
24
|
+
}
|
|
25
|
+
declare function buildToolResponse(payload: unknown): {
|
|
26
|
+
content: {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
declare function toArray<T>(value: T | T[] | undefined): T[] | undefined;
|
|
32
|
+
export { PaperScannerClient, buildToolResponse, toArray };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
class PaperScannerClient {
|
|
2
|
+
apiToken;
|
|
3
|
+
baseUrl;
|
|
4
|
+
defaultDb;
|
|
5
|
+
constructor() {
|
|
6
|
+
this.baseUrl = this.normalizeBaseUrl(process.env.PAPER_SCANNER_API_URL ?? "http://localhost:8000");
|
|
7
|
+
this.apiToken = this.readOptionalEnv("PAPER_SCANNER_API_TOKEN");
|
|
8
|
+
this.defaultDb = this.readOptionalEnv("PAPER_SCANNER_DB");
|
|
9
|
+
}
|
|
10
|
+
getDefaultDb() {
|
|
11
|
+
return this.defaultDb;
|
|
12
|
+
}
|
|
13
|
+
async delete(path, options = {}) {
|
|
14
|
+
return this.request("DELETE", path, options);
|
|
15
|
+
}
|
|
16
|
+
async get(path, options = {}) {
|
|
17
|
+
return this.request("GET", path, options);
|
|
18
|
+
}
|
|
19
|
+
async post(path, options = {}) {
|
|
20
|
+
return this.request("POST", path, options);
|
|
21
|
+
}
|
|
22
|
+
appendQueryParams(searchParams, query) {
|
|
23
|
+
if (!query) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const [key, value] of Object.entries(query)) {
|
|
27
|
+
if (value === undefined || value === null) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (const item of value) {
|
|
32
|
+
searchParams.append(key, String(item));
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
searchParams.set(key, String(value));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
buildUrl(path, options) {
|
|
40
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
41
|
+
const url = new URL(`/api${normalizedPath}`, this.baseUrl);
|
|
42
|
+
const db = options.db ?? this.defaultDb;
|
|
43
|
+
if (db) {
|
|
44
|
+
url.searchParams.set("db", db);
|
|
45
|
+
}
|
|
46
|
+
this.appendQueryParams(url.searchParams, options.query);
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
normalizeBaseUrl(value) {
|
|
50
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
51
|
+
}
|
|
52
|
+
readOptionalEnv(name) {
|
|
53
|
+
const value = process.env[name]?.trim();
|
|
54
|
+
return value ? value : null;
|
|
55
|
+
}
|
|
56
|
+
async request(method, path, options) {
|
|
57
|
+
if (options.auth && !this.apiToken) {
|
|
58
|
+
throw new Error("PAPER_SCANNER_API_TOKEN is required for authenticated tools");
|
|
59
|
+
}
|
|
60
|
+
const headers = new Headers();
|
|
61
|
+
if (options.auth && this.apiToken) {
|
|
62
|
+
headers.set("Authorization", `Bearer ${this.apiToken}`);
|
|
63
|
+
}
|
|
64
|
+
const init = {
|
|
65
|
+
method,
|
|
66
|
+
headers,
|
|
67
|
+
};
|
|
68
|
+
if (options.body !== undefined) {
|
|
69
|
+
headers.set("Content-Type", "application/json");
|
|
70
|
+
init.body = JSON.stringify(options.body);
|
|
71
|
+
}
|
|
72
|
+
const response = await fetch(this.buildUrl(path, options), init);
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const suffix = text ? `: ${text}` : "";
|
|
76
|
+
throw new Error(`Paper Scanner API request failed with ${response.status} ${response.statusText}${suffix}`);
|
|
77
|
+
}
|
|
78
|
+
if (!text) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function buildToolResponse(payload) {
|
|
85
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function toArray(value) {
|
|
96
|
+
if (value === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return Array.isArray(value) ? value : [value];
|
|
100
|
+
}
|
|
101
|
+
export { PaperScannerClient, buildToolResponse, toArray };
|
|
102
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 { PaperScannerClient } from "./client.js";
|
|
5
|
+
import { registerArticleTools } from "./tools/articles.js";
|
|
6
|
+
import { registerFavoriteTools } from "./tools/favorites.js";
|
|
7
|
+
import { registerJournalTools } from "./tools/journals.js";
|
|
8
|
+
import { registerMetaTools } from "./tools/meta.js";
|
|
9
|
+
import { registerWeeklyTools } from "./tools/weekly.js";
|
|
10
|
+
async function main() {
|
|
11
|
+
const server = new McpServer({
|
|
12
|
+
name: "paper-scanner-mcp",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
});
|
|
15
|
+
const client = new PaperScannerClient();
|
|
16
|
+
registerArticleTools(server, client);
|
|
17
|
+
registerJournalTools(server, client);
|
|
18
|
+
registerMetaTools(server, client);
|
|
19
|
+
registerWeeklyTools(server, client);
|
|
20
|
+
registerFavoriteTools(server, client);
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
}
|
|
24
|
+
main().catch((error) => {
|
|
25
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
26
|
+
console.error(message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,mBAAmB;QACzB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;IAExC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEtC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtF,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { buildToolResponse, toArray } from "../client.js";
|
|
3
|
+
const articleIdSchema = z.number().int().positive();
|
|
4
|
+
const dateSchema = z.string().trim().min(1);
|
|
5
|
+
const databaseSchema = z.string().trim().min(1);
|
|
6
|
+
const articleListSchema = z.object({
|
|
7
|
+
area: z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]).optional(),
|
|
8
|
+
date_from: dateSchema.optional(),
|
|
9
|
+
date_to: dateSchema.optional(),
|
|
10
|
+
db: databaseSchema.optional(),
|
|
11
|
+
journal_id: z.union([articleIdSchema, z.array(articleIdSchema).min(1)]).optional(),
|
|
12
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
13
|
+
open_access: z.boolean().optional(),
|
|
14
|
+
q: z.string().trim().min(1).optional(),
|
|
15
|
+
year: z.number().int().nonnegative().optional(),
|
|
16
|
+
});
|
|
17
|
+
function registerArticleTools(server, client) {
|
|
18
|
+
server.registerTool("search_articles", {
|
|
19
|
+
description: "Search articles in the Paper Scanner index.",
|
|
20
|
+
inputSchema: articleListSchema,
|
|
21
|
+
}, async (params) => {
|
|
22
|
+
const result = await client.get("/articles", {
|
|
23
|
+
db: params.db,
|
|
24
|
+
query: {
|
|
25
|
+
area: toArray(params.area),
|
|
26
|
+
date_from: params.date_from,
|
|
27
|
+
date_to: params.date_to,
|
|
28
|
+
journal_id: toArray(params.journal_id),
|
|
29
|
+
limit: params.limit,
|
|
30
|
+
open_access: params.open_access,
|
|
31
|
+
q: params.q,
|
|
32
|
+
year: params.year,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return buildToolResponse(result);
|
|
36
|
+
});
|
|
37
|
+
server.registerTool("get_article", {
|
|
38
|
+
description: "Get a single article by ID.",
|
|
39
|
+
inputSchema: z.object({
|
|
40
|
+
article_id: articleIdSchema,
|
|
41
|
+
db: databaseSchema.optional(),
|
|
42
|
+
}),
|
|
43
|
+
}, async ({ article_id, db }) => {
|
|
44
|
+
const result = await client.get(`/articles/${article_id}`, {
|
|
45
|
+
db,
|
|
46
|
+
});
|
|
47
|
+
return buildToolResponse(result);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export { registerArticleTools };
|
|
51
|
+
//# sourceMappingURL=articles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"articles.js","sourceRoot":"","sources":["../../src/tools/articles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { buildToolResponse } from "../client.js";
|
|
3
|
+
const articleIdSchema = z.number().int().positive();
|
|
4
|
+
const folderIdSchema = z.number().int().positive();
|
|
5
|
+
const databaseNameSchema = z.string().trim().min(1);
|
|
6
|
+
function registerFavoriteTools(server, client) {
|
|
7
|
+
server.registerTool("list_folders", {
|
|
8
|
+
description: "List favorite folders for the authenticated Paper Scanner user.",
|
|
9
|
+
inputSchema: z.object({}),
|
|
10
|
+
}, async () => {
|
|
11
|
+
const result = await client.get("/favorites/folders", {
|
|
12
|
+
auth: true,
|
|
13
|
+
});
|
|
14
|
+
return buildToolResponse(result);
|
|
15
|
+
});
|
|
16
|
+
server.registerTool("add_favorite", {
|
|
17
|
+
description: "Add an article to a favorite folder for the authenticated user.",
|
|
18
|
+
inputSchema: z.object({
|
|
19
|
+
article_id: articleIdSchema,
|
|
20
|
+
db_name: databaseNameSchema.optional(),
|
|
21
|
+
folder_id: folderIdSchema,
|
|
22
|
+
}),
|
|
23
|
+
}, async ({ article_id, db_name, folder_id }) => {
|
|
24
|
+
const result = await client.post(`/favorites/folders/${folder_id}/articles`, {
|
|
25
|
+
auth: true,
|
|
26
|
+
body: {
|
|
27
|
+
article_id,
|
|
28
|
+
db_name: db_name ?? client.getDefaultDb() ?? "",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return buildToolResponse(result);
|
|
32
|
+
});
|
|
33
|
+
server.registerTool("remove_favorite", {
|
|
34
|
+
description: "Remove an article from a favorite folder for the authenticated user.",
|
|
35
|
+
inputSchema: z.object({
|
|
36
|
+
article_id: articleIdSchema,
|
|
37
|
+
db_name: databaseNameSchema.optional(),
|
|
38
|
+
folder_id: folderIdSchema,
|
|
39
|
+
}),
|
|
40
|
+
}, async ({ article_id, db_name, folder_id }) => {
|
|
41
|
+
const result = await client.delete(`/favorites/folders/${folder_id}/articles/${article_id}`, {
|
|
42
|
+
auth: true,
|
|
43
|
+
query: {
|
|
44
|
+
db_name: db_name ?? client.getDefaultDb() ?? "",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return buildToolResponse(result);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export { registerFavoriteTools };
|
|
51
|
+
//# sourceMappingURL=favorites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"favorites.js","sourceRoot":"","sources":["../../src/tools/favorites.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAsB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;AACpD,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;AACnD,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAEpD,SAAS,qBAAqB,CAC5B,MAAiB,EACjB,MAA0B;IAE1B,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,iEAAiE;QAC9E,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;KAC1B,EACD,KAAK,IAAI,EAAE;QACT,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE;YACpD,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,iEAAiE;QAC9E,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,UAAU,EAAE,eAAe;YAC3B,OAAO,EAAE,kBAAkB,CAAC,QAAQ,EAAE;YACtC,SAAS,EAAE,cAAc;SAC1B,CAAC;KACH,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE;QAC3C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,sBAAsB,SAAS,WAAW,EAAE;YAC3E,IAAI,EAAE,IAAI;YACV,IAAI,EAAE;gBACJ,UAAU;gBACV,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,YAAY,EAAE,IAAI,EAAE;aAChD;SACF,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,WAAW,EAAE,sEAAsE;QACnF,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,UAAU,EAAE,eAAe;YAC3B,OAAO,EAAE,kBAAkB,CAAC,QAAQ,EAAE;YACtC,SAAS,EAAE,cAAc;SAC1B,CAAC;KACH,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE;QAC3C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAChC,sBAAsB,SAAS,aAAa,UAAU,EAAE,EACxD;YACE,IAAI,EAAE,IAAI;YACV,KAAK,EAAE;gBACL,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,YAAY,EAAE,IAAI,EAAE;aAChD;SACF,CACF,CAAC;QAEF,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { buildToolResponse } from "../client.js";
|
|
3
|
+
const databaseSchema = z.string().trim().min(1);
|
|
4
|
+
function registerJournalTools(server, client) {
|
|
5
|
+
server.registerTool("list_journals", {
|
|
6
|
+
description: "List journals from the selected Paper Scanner database.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
db: databaseSchema.optional(),
|
|
9
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
10
|
+
offset: z.number().int().min(0).optional(),
|
|
11
|
+
}),
|
|
12
|
+
}, async ({ db, limit, offset }) => {
|
|
13
|
+
const result = await client.get("/journals", {
|
|
14
|
+
db,
|
|
15
|
+
query: {
|
|
16
|
+
limit,
|
|
17
|
+
offset,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return buildToolResponse(result);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export { registerJournalTools };
|
|
24
|
+
//# sourceMappingURL=journals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journals.js","sourceRoot":"","sources":["../../src/tools/journals.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAsB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAEhD,SAAS,oBAAoB,CAC3B,MAAiB,EACjB,MAA0B;IAE1B,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,WAAW,EAAE,yDAAyD;QACtE,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,EAAE,EAAE,cAAc,CAAC,QAAQ,EAAE;YAC7B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;YAClD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;SAC3C,CAAC;KACH,EACD,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC9B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE;YAC3C,EAAE;YACF,KAAK,EAAE;gBACL,KAAK;gBACL,MAAM;aACP;SACF,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { buildToolResponse } from "../client.js";
|
|
3
|
+
const databaseInputSchema = z.object({
|
|
4
|
+
db: z.string().trim().min(1).optional(),
|
|
5
|
+
});
|
|
6
|
+
function registerMetaTools(server, client) {
|
|
7
|
+
server.registerTool("list_databases", {
|
|
8
|
+
description: "List available Paper Scanner SQLite databases.",
|
|
9
|
+
inputSchema: z.object({}),
|
|
10
|
+
}, async () => {
|
|
11
|
+
const result = await client.get("/meta/databases");
|
|
12
|
+
return buildToolResponse(result);
|
|
13
|
+
});
|
|
14
|
+
server.registerTool("list_areas", {
|
|
15
|
+
description: "List research areas for the selected Paper Scanner database.",
|
|
16
|
+
inputSchema: databaseInputSchema,
|
|
17
|
+
}, async ({ db }) => {
|
|
18
|
+
const result = await client.get("/meta/areas", {
|
|
19
|
+
db,
|
|
20
|
+
});
|
|
21
|
+
return buildToolResponse(result);
|
|
22
|
+
});
|
|
23
|
+
server.registerTool("list_years", {
|
|
24
|
+
description: "List publication years for the selected Paper Scanner database.",
|
|
25
|
+
inputSchema: databaseInputSchema,
|
|
26
|
+
}, async ({ db }) => {
|
|
27
|
+
const result = await client.get("/years", {
|
|
28
|
+
db,
|
|
29
|
+
});
|
|
30
|
+
return buildToolResponse(result);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export { registerMetaTools };
|
|
34
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/tools/meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAsB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAEH,SAAS,iBAAiB,CACxB,MAAiB,EACjB,MAA0B;IAE1B,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,WAAW,EAAE,gDAAgD;QAC7D,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;KAC1B,EACD,KAAK,IAAI,EAAE;QACT,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QACnD,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,WAAW,EAAE,8DAA8D;QAC3E,WAAW,EAAE,mBAAmB;KACjC,EACD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE;YAC7C,EAAE;SACH,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,WAAW,EAAE,iEAAiE;QAC9E,WAAW,EAAE,mBAAmB;KACjC,EACD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;QACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;YACxC,EAAE;SACH,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { buildToolResponse } from "../client.js";
|
|
3
|
+
function registerWeeklyTools(server, client) {
|
|
4
|
+
server.registerTool("get_weekly_updates", {
|
|
5
|
+
description: "Get weekly update summaries across all Paper Scanner databases.",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
window_days: z.number().int().min(1).max(31).optional(),
|
|
8
|
+
}),
|
|
9
|
+
}, async ({ window_days }) => {
|
|
10
|
+
const result = await client.get("/weekly-updates", {
|
|
11
|
+
query: {
|
|
12
|
+
window_days,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
return buildToolResponse(result);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export { registerWeeklyTools };
|
|
19
|
+
//# sourceMappingURL=weekly.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weekly.js","sourceRoot":"","sources":["../../src/tools/weekly.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAsB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAErE,SAAS,mBAAmB,CAC1B,MAAiB,EACjB,MAA0B;IAE1B,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,WAAW,EAAE,iEAAiE;QAC9E,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;SACxD,CAAC;KACH,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,iBAAiB,EAAE;YACjD,KAAK,EAAE;gBACL,WAAW;aACZ;SACF,CAAC,CAAC;QAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qianfuv/paper-scanner-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Paper Scanner",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"paper-scanner-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
23
|
+
"zod": "^4.1.11"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.5.2",
|
|
27
|
+
"typescript": "^5.9.2"
|
|
28
|
+
}
|
|
29
|
+
}
|