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 CHANGED
@@ -1,112 +1,117 @@
1
1
  #!/usr/bin/env node
2
2
  // =============================================================================
3
- // OpenCitations MCP Server - Hello World Starter
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", // Server identifier
27
- version: "0.1.0", // Semantic versioning
30
+ name: "opencitations-server",
31
+ version: "0.2.0",
28
32
  });
29
33
  // -----------------------------------------------------------------------------
30
- // TOOL REGISTRATION
34
+ // TOOLS
31
35
  // -----------------------------------------------------------------------------
32
- // MCP servers expose "tools" that LLMs can call.
33
- // Let's register a simple "hello" tool.
34
- // registerTool takes 3 arguments:
35
- // 1. Tool name (string)
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
- // z.string() means this parameter must be a string
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
- // The handler function - this runs when the tool is called
52
- // 'async' means this function can use 'await' for asynchronous operations
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
- type: "text", // 'as const' is a TypeScript assertion for literal types
65
- text: greeting,
66
- },
67
- ],
47
+ content: [{
48
+ type: "text",
49
+ text: `Citation count for ${args.doi}: ${count}`,
50
+ }],
68
51
  };
69
52
  });
70
- // -----------------------------------------------------------------------------
71
- // ANOTHER EXAMPLE: Tool with required parameters
72
- // -----------------------------------------------------------------------------
73
- server.registerTool("echo", {
74
- description: "Echoes back the message you send (useful for testing)",
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
- // No .optional() here - this parameter is required
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
- // TypeScript knows args.message is definitely a string (not undefined)
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: `You said: "${args.message}"`,
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 THE SERVER
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); // Exit with error code
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.0",
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
  }