mcp-server-opencitations 0.1.1 → 0.1.3
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 +107 -30
- package/dist/lib.js +146 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,63 +5,140 @@
|
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
// Import API client functions from lib.ts
|
|
9
|
+
import { getCitation, getCitationCount, getCitations, getReferenceCount, getReferences, getVenueCitationCount, formatDoi, formatIssn, formatCitationAsText, formatCitationsAsText, } from "./lib.js";
|
|
8
10
|
// -----------------------------------------------------------------------------
|
|
9
|
-
// CONFIGURATION
|
|
11
|
+
// CONFIGURATION
|
|
10
12
|
// -----------------------------------------------------------------------------
|
|
11
|
-
// Priority: Command line args > Environment variables
|
|
12
|
-
// process.argv is an array of command-line arguments:
|
|
13
|
-
// [0] = path to node
|
|
14
|
-
// [1] = path to script
|
|
15
|
-
// [2+] = user arguments
|
|
16
|
-
//
|
|
17
|
-
// Example: node dist/index.js --token=abc123
|
|
18
|
-
// process.argv[2] would be "--token=abc123"
|
|
19
13
|
function getAccessToken() {
|
|
20
|
-
// Check command line args first
|
|
21
14
|
const tokenArg = process.argv.find(arg => arg.startsWith('--token='));
|
|
22
15
|
if (tokenArg) {
|
|
23
16
|
return tokenArg.split('=')[1];
|
|
24
17
|
}
|
|
25
|
-
// Fall back to environment variable
|
|
26
18
|
return process.env.OPENCITATIONS_ACCESS_TOKEN;
|
|
27
19
|
}
|
|
28
20
|
const ACCESS_TOKEN = getAccessToken();
|
|
29
21
|
if (!ACCESS_TOKEN) {
|
|
30
|
-
console.error("Warning: No access token set
|
|
22
|
+
console.error("Warning: No access token set (optional but recommended)");
|
|
31
23
|
console.error(" Use: --token=YOUR_TOKEN");
|
|
32
24
|
console.error(" Or set OPENCITATIONS_ACCESS_TOKEN environment variable");
|
|
33
25
|
}
|
|
34
|
-
//
|
|
26
|
+
// -----------------------------------------------------------------------------
|
|
27
|
+
// SERVER SETUP
|
|
28
|
+
// -----------------------------------------------------------------------------
|
|
35
29
|
const server = new McpServer({
|
|
36
30
|
name: "opencitations-server",
|
|
37
|
-
version: "0.
|
|
31
|
+
version: "0.2.0",
|
|
38
32
|
});
|
|
39
33
|
// -----------------------------------------------------------------------------
|
|
40
34
|
// TOOLS
|
|
41
35
|
// -----------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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'.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
doi: z.string().describe("DOI of the paper (e.g., '10.1108/jd-12-2013-0166')"),
|
|
42
|
+
},
|
|
43
|
+
}, async (args) => {
|
|
44
|
+
const id = formatDoi(args.doi);
|
|
45
|
+
const count = await getCitationCount(id, ACCESS_TOKEN);
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Citation count for ${args.doi}: ${count}`,
|
|
50
|
+
}],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
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
|
+
inputSchema: {
|
|
76
|
+
doi: z.string().describe("DOI of the paper (e.g., '10.7717/peerj-cs.421')"),
|
|
77
|
+
},
|
|
78
|
+
}, async (args) => {
|
|
79
|
+
const id = formatDoi(args.doi);
|
|
80
|
+
const count = await getReferenceCount(id, ACCESS_TOKEN);
|
|
81
|
+
return {
|
|
82
|
+
content: [{
|
|
83
|
+
type: "text",
|
|
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 }],
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
// Tool: Get single citation by OCI
|
|
107
|
+
server.registerTool("get_citation", {
|
|
108
|
+
description: "Get metadata for a specific citation using its Open Citation Identifier (OCI). " +
|
|
109
|
+
"An OCI is a unique identifier for a citation link between two papers. " +
|
|
110
|
+
"Example OCI: '06101801781-06180334099'.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
oci: z.string().describe("Open Citation Identifier (e.g., '06101801781-06180334099')"),
|
|
113
|
+
},
|
|
114
|
+
}, async (args) => {
|
|
115
|
+
// Remove 'oci:' prefix if present
|
|
116
|
+
const oci = args.oci.replace(/^oci:/, "");
|
|
117
|
+
const citation = await getCitation(oci, ACCESS_TOKEN);
|
|
118
|
+
if (!citation) {
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text", text: `No citation found for OCI: ${args.oci}` }],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
50
123
|
return {
|
|
51
|
-
content: [{ type: "text", text:
|
|
124
|
+
content: [{ type: "text", text: formatCitationAsText(citation) }],
|
|
52
125
|
};
|
|
53
126
|
});
|
|
54
|
-
|
|
55
|
-
|
|
127
|
+
// Tool: Get venue citation count
|
|
128
|
+
server.registerTool("venue_citation_count", {
|
|
129
|
+
description: "Get the total number of citations for all papers published in a journal/venue. " +
|
|
130
|
+
"Provide an ISSN like '0138-9130' or 'issn:0138-9130'.",
|
|
56
131
|
inputSchema: {
|
|
57
|
-
|
|
132
|
+
issn: z.string().describe("ISSN of the journal (e.g., '0138-9130')"),
|
|
58
133
|
},
|
|
59
134
|
}, async (args) => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
: "Hello!";
|
|
135
|
+
const id = formatIssn(args.issn);
|
|
136
|
+
const count = await getVenueCitationCount(id, ACCESS_TOKEN);
|
|
63
137
|
return {
|
|
64
|
-
content: [{
|
|
138
|
+
content: [{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: `Total citations for venue ${args.issn}: ${count}`,
|
|
141
|
+
}],
|
|
65
142
|
};
|
|
66
143
|
});
|
|
67
144
|
// -----------------------------------------------------------------------------
|
package/dist/lib.js
CHANGED
|
@@ -1,67 +1,167 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
//
|
|
2
|
+
// OpenCitations API Client
|
|
3
3
|
// =============================================================================
|
|
4
|
-
|
|
5
|
-
// Separating logic into lib.ts makes code easier to test and maintain.
|
|
4
|
+
const BASE_URL = "https://api.opencitations.net/index/v2";
|
|
6
5
|
// -----------------------------------------------------------------------------
|
|
7
|
-
//
|
|
6
|
+
// API Client
|
|
8
7
|
// -----------------------------------------------------------------------------
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* Makes a request to the OpenCitations API
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - Whatever you 'return' becomes the resolved value of the Promise
|
|
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
|
|
16
14
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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);
|
|
21
46
|
}
|
|
22
47
|
/**
|
|
23
|
-
*
|
|
48
|
+
* Get all citations (papers that cite the given identifier)
|
|
24
49
|
*
|
|
25
|
-
* @param
|
|
26
|
-
* @
|
|
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
|
|
27
53
|
*/
|
|
28
|
-
export function
|
|
29
|
-
|
|
30
|
-
// - 'resolve' is called when the operation succeeds
|
|
31
|
-
// - setTimeout calls resolve after 'ms' milliseconds
|
|
32
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
export async function getCitations(id, accessToken) {
|
|
55
|
+
return apiRequest(`/citations/${id}`, accessToken);
|
|
33
56
|
}
|
|
34
57
|
/**
|
|
35
|
-
*
|
|
36
|
-
* Later, this will call the real OpenCitations API
|
|
58
|
+
* Get the count of references for a given identifier
|
|
37
59
|
*
|
|
38
|
-
* @param
|
|
39
|
-
* @
|
|
40
|
-
* @
|
|
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
|
|
41
63
|
*/
|
|
42
|
-
export async function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Later, this will make a real HTTP request to OpenCitations API
|
|
47
|
-
if (!doi || doi.trim() === '') {
|
|
48
|
-
throw new Error('DOI cannot be empty');
|
|
64
|
+
export async function getReferenceCount(id, accessToken) {
|
|
65
|
+
const data = await apiRequest(`/reference-count/${id}`, accessToken);
|
|
66
|
+
if (data.length === 0) {
|
|
67
|
+
return 0;
|
|
49
68
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
57
91
|
}
|
|
58
92
|
/**
|
|
59
|
-
*
|
|
93
|
+
* Get the total citation count for all papers in a venue (journal)
|
|
60
94
|
*
|
|
61
|
-
* @param
|
|
62
|
-
* @
|
|
95
|
+
* @param issn - ISSN of the venue (e.g., "issn:0138-9130")
|
|
96
|
+
* @param accessToken - Optional access token
|
|
97
|
+
* @returns Total citation count for the venue
|
|
63
98
|
*/
|
|
64
|
-
export function
|
|
65
|
-
const
|
|
66
|
-
|
|
99
|
+
export async function getVenueCitationCount(issn, accessToken) {
|
|
100
|
+
const data = await apiRequest(`/venue-citation-count/${issn}`, accessToken);
|
|
101
|
+
if (data.length === 0) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
return parseInt(data[0].count, 10);
|
|
105
|
+
}
|
|
106
|
+
// -----------------------------------------------------------------------------
|
|
107
|
+
// Utility Functions
|
|
108
|
+
// -----------------------------------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Format a DOI into the API's expected format
|
|
111
|
+
* Handles both raw DOIs and prefixed DOIs
|
|
112
|
+
*/
|
|
113
|
+
export function formatDoi(doi) {
|
|
114
|
+
// Remove URL prefix if present
|
|
115
|
+
doi = doi.replace(/^https?:\/\/doi\.org\//, "");
|
|
116
|
+
// Add "doi:" prefix if not present
|
|
117
|
+
if (!doi.startsWith("doi:")) {
|
|
118
|
+
return `doi:${doi}`;
|
|
119
|
+
}
|
|
120
|
+
return doi;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Format an ISSN into the API's expected format
|
|
124
|
+
*/
|
|
125
|
+
export function formatIssn(issn) {
|
|
126
|
+
// Add "issn:" prefix if not present
|
|
127
|
+
if (!issn.startsWith("issn:")) {
|
|
128
|
+
return `issn:${issn}`;
|
|
129
|
+
}
|
|
130
|
+
return issn;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Format a single Citation into a readable string
|
|
134
|
+
*/
|
|
135
|
+
export function formatCitationAsText(citation) {
|
|
136
|
+
const lines = [
|
|
137
|
+
`OCI: ${citation.oci}`,
|
|
138
|
+
`Citing: ${citation.citing}`,
|
|
139
|
+
`Cited: ${citation.cited}`,
|
|
140
|
+
`Date: ${citation.creation || "N/A"}`,
|
|
141
|
+
`Timespan: ${citation.timespan || "N/A"}`,
|
|
142
|
+
`Journal self-citation: ${citation.journal_sc || "no"}`,
|
|
143
|
+
`Author self-citation: ${citation.author_sc || "no"}`,
|
|
144
|
+
];
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Format citations into a readable string
|
|
149
|
+
*/
|
|
150
|
+
export function formatCitationsAsText(citations) {
|
|
151
|
+
if (citations.length === 0) {
|
|
152
|
+
return "No citations found.";
|
|
153
|
+
}
|
|
154
|
+
return citations.map((c, i) => {
|
|
155
|
+
const lines = [
|
|
156
|
+
`[${i + 1}] OCI: ${c.oci}`,
|
|
157
|
+
` Citing: ${c.citing}`,
|
|
158
|
+
` Cited: ${c.cited}`,
|
|
159
|
+
` Date: ${c.creation || "N/A"}`,
|
|
160
|
+
];
|
|
161
|
+
if (c.journal_sc === "yes")
|
|
162
|
+
lines.push(" (Journal self-citation)");
|
|
163
|
+
if (c.author_sc === "yes")
|
|
164
|
+
lines.push(" (Author self-citation)");
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}).join("\n\n");
|
|
67
167
|
}
|