mcp-server-opencitations 0.1.0 → 0.1.2
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/index.js +82 -77
- package/dist/lib.js +128 -0
- package/dist/vitest.config.js +13 -0
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -1,112 +1,117 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// =============================================================================
|
|
3
|
-
// OpenCitations MCP Server
|
|
3
|
+
// OpenCitations MCP Server
|
|
4
4
|
// =============================================================================
|
|
5
|
-
// This is a minimal MCP server to help you learn TypeScript and MCP concepts.
|
|
6
|
-
// Comments explain TypeScript features as we go!
|
|
7
|
-
// -----------------------------------------------------------------------------
|
|
8
|
-
// IMPORTS
|
|
9
|
-
// -----------------------------------------------------------------------------
|
|
10
|
-
// In TypeScript, we import modules using ES6 syntax.
|
|
11
|
-
// The "from" path can be a package name or a relative file path.
|
|
12
|
-
// McpServer is the main class that handles MCP protocol communication
|
|
13
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
-
// StdioServerTransport handles communication over stdin/stdout
|
|
15
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
-
// 'z' is Zod - a TypeScript-first schema validation library
|
|
17
|
-
// It lets us define the shape of data and validate it at runtime
|
|
18
7
|
import { z } from "zod";
|
|
8
|
+
// Import API client functions from lib.ts
|
|
9
|
+
import { getCitationCount, getCitations, getReferenceCount, getReferences, formatDoi, formatCitationsAsText, } from "./lib.js";
|
|
10
|
+
// -----------------------------------------------------------------------------
|
|
11
|
+
// CONFIGURATION
|
|
12
|
+
// -----------------------------------------------------------------------------
|
|
13
|
+
function getAccessToken() {
|
|
14
|
+
const tokenArg = process.argv.find(arg => arg.startsWith('--token='));
|
|
15
|
+
if (tokenArg) {
|
|
16
|
+
return tokenArg.split('=')[1];
|
|
17
|
+
}
|
|
18
|
+
return process.env.OPENCITATIONS_ACCESS_TOKEN;
|
|
19
|
+
}
|
|
20
|
+
const ACCESS_TOKEN = getAccessToken();
|
|
21
|
+
if (!ACCESS_TOKEN) {
|
|
22
|
+
console.error("Warning: No access token set (optional but recommended)");
|
|
23
|
+
console.error(" Use: --token=YOUR_TOKEN");
|
|
24
|
+
console.error(" Or set OPENCITATIONS_ACCESS_TOKEN environment variable");
|
|
25
|
+
}
|
|
19
26
|
// -----------------------------------------------------------------------------
|
|
20
27
|
// SERVER SETUP
|
|
21
28
|
// -----------------------------------------------------------------------------
|
|
22
|
-
// Create a new MCP server instance.
|
|
23
|
-
// The object passed to McpServer is called an "object literal" in TypeScript.
|
|
24
|
-
// It has typed properties: 'name' and 'version' are both strings.
|
|
25
29
|
const server = new McpServer({
|
|
26
|
-
name: "opencitations-server",
|
|
27
|
-
version: "0.
|
|
30
|
+
name: "opencitations-server",
|
|
31
|
+
version: "0.2.0",
|
|
28
32
|
});
|
|
29
33
|
// -----------------------------------------------------------------------------
|
|
30
|
-
//
|
|
34
|
+
// TOOLS
|
|
31
35
|
// -----------------------------------------------------------------------------
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// 2. Tool configuration (object with description, inputSchema, etc.)
|
|
37
|
-
// 3. Handler function (async function that runs when tool is called)
|
|
38
|
-
server.registerTool("hello", // Tool name - this is what the LLM will use to call it
|
|
39
|
-
{
|
|
40
|
-
// Human-readable description - helps LLMs understand when to use this tool
|
|
41
|
-
description: "A simple hello world tool that greets the user",
|
|
42
|
-
// inputSchema defines what parameters this tool accepts
|
|
43
|
-
// We use Zod schemas here - they provide both TypeScript types AND runtime validation
|
|
36
|
+
// Tool: Get citation count
|
|
37
|
+
server.registerTool("citation_count", {
|
|
38
|
+
description: "Get the number of citations for a paper (how many papers cite it). " +
|
|
39
|
+
"Provide a DOI like '10.1108/jd-12-2013-0166' or 'doi:10.1108/jd-12-2013-0166'.",
|
|
44
40
|
inputSchema: {
|
|
45
|
-
|
|
46
|
-
// .optional() means it's not required
|
|
47
|
-
// .describe() adds documentation for the LLM
|
|
48
|
-
name: z.string().optional().describe("Name to greet (optional)"),
|
|
41
|
+
doi: z.string().describe("DOI of the paper (e.g., '10.1108/jd-12-2013-0166')"),
|
|
49
42
|
},
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// The parameter 'args' is automatically typed based on our inputSchema!
|
|
54
|
-
async (args) => {
|
|
55
|
-
// TypeScript knows 'args.name' is string | undefined because of our schema
|
|
56
|
-
const greeting = args.name
|
|
57
|
-
? `Hello, ${args.name}! Welcome to OpenCitations MCP server.`
|
|
58
|
-
: "Hello! Welcome to OpenCitations MCP server.";
|
|
59
|
-
// Tools return a result object with a 'content' array
|
|
60
|
-
// Each content item has a 'type' (usually "text") and the actual content
|
|
43
|
+
}, async (args) => {
|
|
44
|
+
const id = formatDoi(args.doi);
|
|
45
|
+
const count = await getCitationCount(id, ACCESS_TOKEN);
|
|
61
46
|
return {
|
|
62
|
-
content: [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
],
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Citation count for ${args.doi}: ${count}`,
|
|
50
|
+
}],
|
|
68
51
|
};
|
|
69
52
|
});
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
// Tool: Get citations (papers that cite this paper)
|
|
54
|
+
server.registerTool("get_citations", {
|
|
55
|
+
description: "Get all papers that cite a given paper. " +
|
|
56
|
+
"Returns a list of citing papers with their DOIs and metadata. " +
|
|
57
|
+
"Provide a DOI like '10.1108/jd-12-2013-0166'.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
doi: z.string().describe("DOI of the paper to find citations for"),
|
|
60
|
+
},
|
|
61
|
+
}, async (args) => {
|
|
62
|
+
const id = formatDoi(args.doi);
|
|
63
|
+
const citations = await getCitations(id, ACCESS_TOKEN);
|
|
64
|
+
const text = citations.length > 0
|
|
65
|
+
? `Found ${citations.length} citations for ${args.doi}:\n\n${formatCitationsAsText(citations)}`
|
|
66
|
+
: `No citations found for ${args.doi}`;
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text }],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
// Tool: Get reference count
|
|
72
|
+
server.registerTool("reference_count", {
|
|
73
|
+
description: "Get the number of references in a paper (how many papers it cites). " +
|
|
74
|
+
"Provide a DOI like '10.7717/peerj-cs.421'.",
|
|
75
75
|
inputSchema: {
|
|
76
|
-
|
|
77
|
-
message: z.string().describe("The message to echo back"),
|
|
76
|
+
doi: z.string().describe("DOI of the paper (e.g., '10.7717/peerj-cs.421')"),
|
|
78
77
|
},
|
|
79
78
|
}, async (args) => {
|
|
80
|
-
|
|
79
|
+
const id = formatDoi(args.doi);
|
|
80
|
+
const count = await getReferenceCount(id, ACCESS_TOKEN);
|
|
81
81
|
return {
|
|
82
|
-
content: [
|
|
83
|
-
{
|
|
82
|
+
content: [{
|
|
84
83
|
type: "text",
|
|
85
|
-
text: `
|
|
86
|
-
},
|
|
87
|
-
|
|
84
|
+
text: `Reference count for ${args.doi}: ${count}`,
|
|
85
|
+
}],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
// Tool: Get references (papers this paper cites)
|
|
89
|
+
server.registerTool("get_references", {
|
|
90
|
+
description: "Get all papers referenced by a given paper. " +
|
|
91
|
+
"Returns a list of referenced papers with their DOIs and metadata. " +
|
|
92
|
+
"Provide a DOI like '10.7717/peerj-cs.421'.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
doi: z.string().describe("DOI of the paper to find references for"),
|
|
95
|
+
},
|
|
96
|
+
}, async (args) => {
|
|
97
|
+
const id = formatDoi(args.doi);
|
|
98
|
+
const references = await getReferences(id, ACCESS_TOKEN);
|
|
99
|
+
const text = references.length > 0
|
|
100
|
+
? `Found ${references.length} references in ${args.doi}:\n\n${formatCitationsAsText(references)}`
|
|
101
|
+
: `No references found for ${args.doi}`;
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text }],
|
|
88
104
|
};
|
|
89
105
|
});
|
|
90
106
|
// -----------------------------------------------------------------------------
|
|
91
|
-
// START
|
|
107
|
+
// START SERVER
|
|
92
108
|
// -----------------------------------------------------------------------------
|
|
93
|
-
// This is an async function that starts our MCP server.
|
|
94
|
-
// In TypeScript, we use 'async function' for functions that return Promises.
|
|
95
109
|
async function main() {
|
|
96
|
-
// Promise<void> is the return type - void means we don't return a value
|
|
97
|
-
// The function is async, so it automatically returns a Promise
|
|
98
|
-
// Create a transport layer for stdin/stdout communication
|
|
99
110
|
const transport = new StdioServerTransport();
|
|
100
|
-
// Connect the server to the transport
|
|
101
|
-
// 'await' pauses execution until the Promise resolves
|
|
102
111
|
await server.connect(transport);
|
|
103
|
-
// Log to stderr (not stdout, which is used for MCP protocol messages)
|
|
104
112
|
console.error("OpenCitations MCP Server running on stdio");
|
|
105
113
|
}
|
|
106
|
-
// Run the main function and handle any errors
|
|
107
|
-
// .catch() handles any rejected promises (errors)
|
|
108
114
|
main().catch((error) => {
|
|
109
|
-
// 'unknown' is a safe type for caught errors in TypeScript
|
|
110
115
|
console.error("Fatal error:", error);
|
|
111
|
-
process.exit(1);
|
|
116
|
+
process.exit(1);
|
|
112
117
|
});
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OpenCitations API Client
|
|
3
|
+
// =============================================================================
|
|
4
|
+
const BASE_URL = "https://api.opencitations.net/index/v2";
|
|
5
|
+
// -----------------------------------------------------------------------------
|
|
6
|
+
// API Client
|
|
7
|
+
// -----------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Makes a request to the OpenCitations API
|
|
10
|
+
*
|
|
11
|
+
* @param endpoint - API endpoint (e.g., "/citations/doi:10.1234/example")
|
|
12
|
+
* @param accessToken - Optional access token for authentication
|
|
13
|
+
* @returns Promise with the JSON response
|
|
14
|
+
*/
|
|
15
|
+
async function apiRequest(endpoint, accessToken) {
|
|
16
|
+
const url = `${BASE_URL}${endpoint}`;
|
|
17
|
+
const headers = {
|
|
18
|
+
"Accept": "application/json",
|
|
19
|
+
};
|
|
20
|
+
// Add authorization header if token is provided
|
|
21
|
+
if (accessToken) {
|
|
22
|
+
headers["Authorization"] = accessToken;
|
|
23
|
+
}
|
|
24
|
+
const response = await fetch(url, { headers });
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
// -----------------------------------------------------------------------------
|
|
31
|
+
// Public Functions
|
|
32
|
+
// -----------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* Get the count of citations for a given identifier
|
|
35
|
+
*
|
|
36
|
+
* @param id - Identifier with prefix (e.g., "doi:10.1108/jd-12-2013-0166")
|
|
37
|
+
* @param accessToken - Optional access token
|
|
38
|
+
* @returns Number of citations
|
|
39
|
+
*/
|
|
40
|
+
export async function getCitationCount(id, accessToken) {
|
|
41
|
+
const data = await apiRequest(`/citation-count/${id}`, accessToken);
|
|
42
|
+
if (data.length === 0) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
return parseInt(data[0].count, 10);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get all citations (papers that cite the given identifier)
|
|
49
|
+
*
|
|
50
|
+
* @param id - Identifier with prefix (e.g., "doi:10.1108/jd-12-2013-0166")
|
|
51
|
+
* @param accessToken - Optional access token
|
|
52
|
+
* @returns Array of Citation objects
|
|
53
|
+
*/
|
|
54
|
+
export async function getCitations(id, accessToken) {
|
|
55
|
+
return apiRequest(`/citations/${id}`, accessToken);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the count of references for a given identifier
|
|
59
|
+
*
|
|
60
|
+
* @param id - Identifier with prefix (e.g., "doi:10.7717/peerj-cs.421")
|
|
61
|
+
* @param accessToken - Optional access token
|
|
62
|
+
* @returns Number of references
|
|
63
|
+
*/
|
|
64
|
+
export async function getReferenceCount(id, accessToken) {
|
|
65
|
+
const data = await apiRequest(`/reference-count/${id}`, accessToken);
|
|
66
|
+
if (data.length === 0) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
return parseInt(data[0].count, 10);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get all references (papers cited by the given identifier)
|
|
73
|
+
*
|
|
74
|
+
* @param id - Identifier with prefix (e.g., "doi:10.7717/peerj-cs.421")
|
|
75
|
+
* @param accessToken - Optional access token
|
|
76
|
+
* @returns Array of Citation objects
|
|
77
|
+
*/
|
|
78
|
+
export async function getReferences(id, accessToken) {
|
|
79
|
+
return apiRequest(`/references/${id}`, accessToken);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get metadata for a specific citation by OCI
|
|
83
|
+
*
|
|
84
|
+
* @param oci - Open Citation Identifier (e.g., "06101801781-06180334099")
|
|
85
|
+
* @param accessToken - Optional access token
|
|
86
|
+
* @returns Citation metadata
|
|
87
|
+
*/
|
|
88
|
+
export async function getCitation(oci, accessToken) {
|
|
89
|
+
const data = await apiRequest(`/citation/${oci}`, accessToken);
|
|
90
|
+
return data.length > 0 ? data[0] : null;
|
|
91
|
+
}
|
|
92
|
+
// -----------------------------------------------------------------------------
|
|
93
|
+
// Utility Functions
|
|
94
|
+
// -----------------------------------------------------------------------------
|
|
95
|
+
/**
|
|
96
|
+
* Format a DOI into the API's expected format
|
|
97
|
+
* Handles both raw DOIs and prefixed DOIs
|
|
98
|
+
*/
|
|
99
|
+
export function formatDoi(doi) {
|
|
100
|
+
// Remove URL prefix if present
|
|
101
|
+
doi = doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
102
|
+
// Add "doi:" prefix if not present
|
|
103
|
+
if (!doi.startsWith("doi:")) {
|
|
104
|
+
return `doi:${doi}`;
|
|
105
|
+
}
|
|
106
|
+
return doi;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format citations into a readable string
|
|
110
|
+
*/
|
|
111
|
+
export function formatCitationsAsText(citations) {
|
|
112
|
+
if (citations.length === 0) {
|
|
113
|
+
return "No citations found.";
|
|
114
|
+
}
|
|
115
|
+
return citations.map((c, i) => {
|
|
116
|
+
const lines = [
|
|
117
|
+
`[${i + 1}] OCI: ${c.oci}`,
|
|
118
|
+
` Citing: ${c.citing}`,
|
|
119
|
+
` Cited: ${c.cited}`,
|
|
120
|
+
` Date: ${c.creation || "N/A"}`,
|
|
121
|
+
];
|
|
122
|
+
if (c.journal_sc === "yes")
|
|
123
|
+
lines.push(" (Journal self-citation)");
|
|
124
|
+
if (c.author_sc === "yes")
|
|
125
|
+
lines.push(" (Author self-citation)");
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}).join("\n\n");
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
export default defineConfig({
|
|
3
|
+
test: {
|
|
4
|
+
globals: true,
|
|
5
|
+
environment: 'node',
|
|
6
|
+
include: ['**/__tests__/**/*.test.ts'],
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
include: ['**/*.ts'],
|
|
10
|
+
exclude: ['**/__tests__/**', '**/dist/**', 'vitest.config.ts'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-opencitations",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCP server for OpenCitations API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ahmeshaf",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/ahmeshaf/mcp-server-opencitations"
|
|
16
|
+
"url": "git+https://github.com/ahmeshaf/mcp-server-opencitations.git"
|
|
17
17
|
},
|
|
18
18
|
"type": "module",
|
|
19
19
|
"bin": {
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
27
27
|
"prepare": "npm run build",
|
|
28
|
-
"watch": "tsc --watch"
|
|
28
|
+
"watch": "tsc --watch",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"test:coverage": "vitest run --coverage"
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|
|
31
34
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
@@ -33,7 +36,9 @@
|
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
38
|
"@types/node": "^22",
|
|
39
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
36
40
|
"shx": "^0.3.4",
|
|
37
|
-
"typescript": "^5.8.2"
|
|
41
|
+
"typescript": "^5.8.2",
|
|
42
|
+
"vitest": "^2.1.8"
|
|
38
43
|
}
|
|
39
44
|
}
|