inaturalist-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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.js +318 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ranfang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# iNaturalist MCP
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
Read-only Model Context Protocol server for the public iNaturalist API.
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
- `search_observations`: Search public observations by taxon, place, user, date, location, or text.
|
|
12
|
+
- `get_observation`: Fetch one observation by ID.
|
|
13
|
+
- `search_taxa`: Search taxa by scientific name, common name, rank, or iconic taxon.
|
|
14
|
+
- `get_taxon`: Fetch one taxon by ID.
|
|
15
|
+
- `get_observation_species_counts`: Fetch species counts for observation filters.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
From npm:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g inaturalist-mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
From source:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
npm run build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Run
|
|
33
|
+
|
|
34
|
+
Stdio transport, for local MCP clients:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
inaturalist-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
HTTP transport, for deployed MCP clients:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
node dist/index.js --transport http --host 127.0.0.1 --port 8890
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also configure HTTP mode with environment variables:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
MCP_TRANSPORT=http HOST=0.0.0.0 PORT=8890 node dist/index.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The HTTP MCP endpoint is:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
http://127.0.0.1:8890/mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Client Config
|
|
59
|
+
|
|
60
|
+
Example stdio MCP client configuration:
|
|
61
|
+
|
|
62
|
+
Using npm:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"inaturalist": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["-y", "inaturalist-mcp"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Using a local checkout:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"inaturalist": {
|
|
81
|
+
"command": "node",
|
|
82
|
+
"args": ["/path/to/inaturalist-mcp/dist/index.js"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Example Streamable HTTP MCP client URL:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
http://127.0.0.1:8890/mcp
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Notes
|
|
95
|
+
|
|
96
|
+
This server only wraps public read endpoints and does not require OAuth. Authenticated write operations are intentionally out of scope for the initial version.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import * as z from "zod/v4";
|
|
9
|
+
const API_BASE_URL = "https://api.inaturalist.org/v1";
|
|
10
|
+
const USER_AGENT = "inaturalist-mcp/1.0 (https://github.com/ufo2243/inaturalist-mcp)";
|
|
11
|
+
class INaturalistApiError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
body;
|
|
14
|
+
constructor(message, status, body) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function buildUrl(path, params = {}) {
|
|
21
|
+
const url = new URL(`${API_BASE_URL}/${path.replace(/^\/+/, "")}`);
|
|
22
|
+
for (const [key, value] of Object.entries(params)) {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
if (item !== undefined && item !== null && item !== "") {
|
|
26
|
+
url.searchParams.append(key, String(item));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
32
|
+
url.searchParams.set(key, String(value));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
async function getJson(path, params = {}) {
|
|
38
|
+
const url = buildUrl(path, params);
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
headers: {
|
|
41
|
+
Accept: "application/json",
|
|
42
|
+
"User-Agent": USER_AGENT,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const body = await response.text();
|
|
47
|
+
throw new INaturalistApiError(`iNaturalist API request failed with HTTP ${response.status}`, response.status, body.slice(0, 1000));
|
|
48
|
+
}
|
|
49
|
+
return response.json();
|
|
50
|
+
}
|
|
51
|
+
function toolResult(data) {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: JSON.stringify(data, null, 2),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
structuredContent: data,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function errorResult(error) {
|
|
63
|
+
const message = error instanceof INaturalistApiError
|
|
64
|
+
? `${error.message}\n\n${error.body}`
|
|
65
|
+
: error instanceof Error
|
|
66
|
+
? error.message
|
|
67
|
+
: String(error);
|
|
68
|
+
return {
|
|
69
|
+
isError: true,
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: message,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const paginationSchema = {
|
|
79
|
+
page: z.number().int().min(1).optional().describe("Page number. Defaults to 1."),
|
|
80
|
+
per_page: z
|
|
81
|
+
.number()
|
|
82
|
+
.int()
|
|
83
|
+
.min(1)
|
|
84
|
+
.max(50)
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Results per page, capped at 50. Defaults to iNaturalist's API default."),
|
|
87
|
+
};
|
|
88
|
+
function createServer() {
|
|
89
|
+
const server = new McpServer({
|
|
90
|
+
name: "inaturalist-mcp",
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
});
|
|
93
|
+
server.registerTool("search_observations", {
|
|
94
|
+
description: "Search public iNaturalist observations by taxon, place, user, date, location, or free text.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
q: z.string().optional().describe("Free-text search query."),
|
|
97
|
+
taxon_id: z.number().int().positive().optional().describe("iNaturalist taxon ID."),
|
|
98
|
+
place_id: z.number().int().positive().optional().describe("iNaturalist place ID."),
|
|
99
|
+
user_id: z.string().optional().describe("iNaturalist username or user ID."),
|
|
100
|
+
lat: z.number().min(-90).max(90).optional().describe("Latitude for geo search."),
|
|
101
|
+
lng: z.number().min(-180).max(180).optional().describe("Longitude for geo search."),
|
|
102
|
+
radius: z.number().positive().optional().describe("Radius in kilometers when lat/lng are provided."),
|
|
103
|
+
d1: z.string().optional().describe("Observed date lower bound, YYYY-MM-DD."),
|
|
104
|
+
d2: z.string().optional().describe("Observed date upper bound, YYYY-MM-DD."),
|
|
105
|
+
observed_on: z.string().optional().describe("Exact observed date, YYYY-MM-DD."),
|
|
106
|
+
quality_grade: z
|
|
107
|
+
.enum(["casual", "needs_id", "research"])
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("Observation quality grade."),
|
|
110
|
+
order_by: z
|
|
111
|
+
.enum(["created_at", "observed_on", "species_guess", "votes", "id"])
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("Sort field."),
|
|
114
|
+
order: z.enum(["asc", "desc"]).optional().describe("Sort direction."),
|
|
115
|
+
...paginationSchema,
|
|
116
|
+
},
|
|
117
|
+
}, async (args) => {
|
|
118
|
+
try {
|
|
119
|
+
return toolResult(await getJson("observations", args));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return errorResult(error);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
server.registerTool("get_observation", {
|
|
126
|
+
description: "Get one public iNaturalist observation by observation ID.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
id: z.number().int().positive().describe("iNaturalist observation ID."),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ id }) => {
|
|
131
|
+
try {
|
|
132
|
+
return toolResult(await getJson(`observations/${id}`));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return errorResult(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.registerTool("search_taxa", {
|
|
139
|
+
description: "Search iNaturalist taxa by name, common name, rank, or iconic taxon.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
q: z.string().optional().describe("Taxon name or common name query."),
|
|
142
|
+
rank: z
|
|
143
|
+
.enum(["kingdom", "phylum", "class", "order", "family", "genus", "species", "subspecies"])
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Taxonomic rank filter."),
|
|
146
|
+
iconic_taxa: z
|
|
147
|
+
.string()
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("Comma-separated iconic taxa, e.g. Aves,Mammalia,Plantae."),
|
|
150
|
+
locale: z.string().optional().describe("Locale for common names, e.g. en, zh-CN."),
|
|
151
|
+
...paginationSchema,
|
|
152
|
+
},
|
|
153
|
+
}, async (args) => {
|
|
154
|
+
try {
|
|
155
|
+
return toolResult(await getJson("taxa", args));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return errorResult(error);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("get_taxon", {
|
|
162
|
+
description: "Get one iNaturalist taxon by taxon ID.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
id: z.number().int().positive().describe("iNaturalist taxon ID."),
|
|
165
|
+
locale: z.string().optional().describe("Locale for common names, e.g. en, zh-CN."),
|
|
166
|
+
},
|
|
167
|
+
}, async ({ id, locale }) => {
|
|
168
|
+
try {
|
|
169
|
+
return toolResult(await getJson(`taxa/${id}`, { locale }));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return errorResult(error);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
server.registerTool("get_observation_species_counts", {
|
|
176
|
+
description: "Get species counts from public iNaturalist observations for a place, taxon, user, date range, or location.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
taxon_id: z.number().int().positive().optional().describe("iNaturalist taxon ID."),
|
|
179
|
+
place_id: z.number().int().positive().optional().describe("iNaturalist place ID."),
|
|
180
|
+
user_id: z.string().optional().describe("iNaturalist username or user ID."),
|
|
181
|
+
lat: z.number().min(-90).max(90).optional().describe("Latitude for geo search."),
|
|
182
|
+
lng: z.number().min(-180).max(180).optional().describe("Longitude for geo search."),
|
|
183
|
+
radius: z.number().positive().optional().describe("Radius in kilometers when lat/lng are provided."),
|
|
184
|
+
d1: z.string().optional().describe("Observed date lower bound, YYYY-MM-DD."),
|
|
185
|
+
d2: z.string().optional().describe("Observed date upper bound, YYYY-MM-DD."),
|
|
186
|
+
locale: z.string().optional().describe("Locale for common names, e.g. en, zh-CN."),
|
|
187
|
+
...paginationSchema,
|
|
188
|
+
},
|
|
189
|
+
}, async (args) => {
|
|
190
|
+
try {
|
|
191
|
+
return toolResult(await getJson("observations/species_counts", args));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return errorResult(error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return server;
|
|
198
|
+
}
|
|
199
|
+
function getArgValue(name) {
|
|
200
|
+
const index = process.argv.indexOf(name);
|
|
201
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
202
|
+
}
|
|
203
|
+
function getTransportMode() {
|
|
204
|
+
const value = getArgValue("--transport") ?? process.env.MCP_TRANSPORT ?? "stdio";
|
|
205
|
+
if (value === "stdio" || value === "http") {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`Unsupported transport "${value}". Use "stdio" or "http".`);
|
|
209
|
+
}
|
|
210
|
+
function getPort() {
|
|
211
|
+
const value = getArgValue("--port") ?? process.env.PORT ?? "3000";
|
|
212
|
+
const port = Number(value);
|
|
213
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
214
|
+
throw new Error(`Invalid port "${value}".`);
|
|
215
|
+
}
|
|
216
|
+
return port;
|
|
217
|
+
}
|
|
218
|
+
function getHost() {
|
|
219
|
+
return getArgValue("--host") ?? process.env.HOST ?? "127.0.0.1";
|
|
220
|
+
}
|
|
221
|
+
async function runStdio() {
|
|
222
|
+
const server = createServer();
|
|
223
|
+
const transport = new StdioServerTransport();
|
|
224
|
+
await server.connect(transport);
|
|
225
|
+
console.error("iNaturalist MCP server running on stdio");
|
|
226
|
+
}
|
|
227
|
+
async function runHttp() {
|
|
228
|
+
const host = getHost();
|
|
229
|
+
const port = getPort();
|
|
230
|
+
const app = createMcpExpressApp({ host });
|
|
231
|
+
const transports = {};
|
|
232
|
+
app.post("/mcp", async (req, res) => {
|
|
233
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
234
|
+
try {
|
|
235
|
+
let transport;
|
|
236
|
+
if (typeof sessionId === "string" && transports[sessionId]) {
|
|
237
|
+
transport = transports[sessionId];
|
|
238
|
+
}
|
|
239
|
+
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
240
|
+
transport = new StreamableHTTPServerTransport({
|
|
241
|
+
sessionIdGenerator: () => randomUUID(),
|
|
242
|
+
onsessioninitialized: (newSessionId) => {
|
|
243
|
+
transports[newSessionId] = transport;
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
transport.onclose = () => {
|
|
247
|
+
const closedSessionId = transport.sessionId;
|
|
248
|
+
if (closedSessionId) {
|
|
249
|
+
delete transports[closedSessionId];
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const server = createServer();
|
|
253
|
+
await server.connect(transport);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
res.status(400).json({
|
|
257
|
+
jsonrpc: "2.0",
|
|
258
|
+
error: {
|
|
259
|
+
code: -32000,
|
|
260
|
+
message: "Bad Request: missing session ID or initialize request",
|
|
261
|
+
},
|
|
262
|
+
id: null,
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
await transport.handleRequest(req, res, req.body);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
console.error("Error handling MCP request:", error);
|
|
270
|
+
if (!res.headersSent) {
|
|
271
|
+
res.status(500).json({
|
|
272
|
+
jsonrpc: "2.0",
|
|
273
|
+
error: {
|
|
274
|
+
code: -32603,
|
|
275
|
+
message: "Internal server error",
|
|
276
|
+
},
|
|
277
|
+
id: null,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
app.get("/mcp", async (req, res) => {
|
|
283
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
284
|
+
if (typeof sessionId !== "string" || !transports[sessionId]) {
|
|
285
|
+
res.status(400).send("Invalid or missing session ID");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await transports[sessionId].handleRequest(req, res);
|
|
289
|
+
});
|
|
290
|
+
app.delete("/mcp", async (req, res) => {
|
|
291
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
292
|
+
if (typeof sessionId !== "string" || !transports[sessionId]) {
|
|
293
|
+
res.status(400).send("Invalid or missing session ID");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
await transports[sessionId].handleRequest(req, res);
|
|
297
|
+
});
|
|
298
|
+
const httpServer = app.listen(port, host, () => {
|
|
299
|
+
console.error(`iNaturalist MCP server listening at http://${host}:${port}/mcp`);
|
|
300
|
+
});
|
|
301
|
+
process.on("SIGINT", async () => {
|
|
302
|
+
for (const transport of Object.values(transports)) {
|
|
303
|
+
await transport.close();
|
|
304
|
+
}
|
|
305
|
+
httpServer.close(() => process.exit(0));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function main() {
|
|
309
|
+
if (getTransportMode() === "http") {
|
|
310
|
+
await runHttp();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
await runStdio();
|
|
314
|
+
}
|
|
315
|
+
main().catch((error) => {
|
|
316
|
+
console.error("Server error:", error);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inaturalist-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Read-only Model Context Protocol server for the iNaturalist API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"inaturalist-mcp": "dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/ufo2243/inaturalist-mcp.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"inaturalist",
|
|
32
|
+
"biodiversity"
|
|
33
|
+
],
|
|
34
|
+
"author": "ranfang",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/ufo2243/inaturalist-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/ufo2243/inaturalist-mcp#readme",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
+
"express": "^5.2.1",
|
|
47
|
+
"zod": "^4.4.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/express": "^5.0.6",
|
|
51
|
+
"@types/node": "^25.6.0",
|
|
52
|
+
"typescript": "^6.0.3"
|
|
53
|
+
}
|
|
54
|
+
}
|