copyseeker-mcp-server 1.0.4 → 2.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 +1 -1
- package/README.md +40 -122
- package/build/client.js +121 -0
- package/build/index.js +17 -0
- package/build/server.js +84 -0
- package/package.json +52 -53
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,159 +1,77 @@
|
|
|
1
1
|
# Copyseeker MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Give Claude, Cursor and other [MCP](https://modelcontextprotocol.io) clients three image tools from [Copyseeker](https://copyseeker.net):
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
| Tool | What it does | Cost |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `reverse_image_search` | Finds the web pages that show an image, ranked by prominence, with the copies each page uses | 1 search |
|
|
8
|
+
| `detect_ai_image` | Tells whether an image is AI-generated, with a confidence score | 0.01 searches |
|
|
9
|
+
| `detect_nsfw` | Tells whether an image contains nudity or sexual content, with a confidence score | 0.01 searches |
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
- 🖼️ **Multiple Input Methods**: Search by URL or upload images directly
|
|
9
|
-
- 📊 **Detailed Results**: Get comprehensive metadata about found images
|
|
10
|
-
- ⚡ **Fast & Reliable**: Powered by Copyseeker's robust API infrastructure
|
|
11
|
-
- 🔒 **Secure**: Your API key stays local, never shared
|
|
11
|
+
Each tool takes a direct image URL or the path to a local image file (up to 20 MB). Calls draw from the same search balance as the website, Monitor and the [Copyseeker API](https://copyseeker.net/apis).
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Requirements
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
- Node.js 20 or newer
|
|
16
|
+
- A paid Copyseeker plan and an API key: create one under **Account → Developers** at [copyseeker.net/account](https://copyseeker.net/account#api)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
npm install -g copyseeker-mcp-server
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### 2. Get Your API Key
|
|
18
|
+
## Setup
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
2. Subscribe to a plan (free tier available)
|
|
25
|
-
3. Copy your RapidAPI key
|
|
20
|
+
### Claude Code
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
```bash
|
|
23
|
+
claude mcp add copyseeker -e COPYSEEKER_API_KEY=your_api_key -- npx -y copyseeker-mcp-server
|
|
24
|
+
```
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
### Claude Desktop
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
28
|
+
Add this to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`), then restart Claude Desktop:
|
|
33
29
|
|
|
34
30
|
```json
|
|
35
31
|
{
|
|
36
32
|
"mcpServers": {
|
|
37
33
|
"copyseeker": {
|
|
38
|
-
"command": "
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["-y", "copyseeker-mcp-server"],
|
|
39
36
|
"env": {
|
|
40
|
-
"
|
|
37
|
+
"COPYSEEKER_API_KEY": "your_api_key"
|
|
41
38
|
}
|
|
42
39
|
}
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
###
|
|
48
|
-
|
|
49
|
-
The Copyseeker tools will now be available in Claude!
|
|
50
|
-
|
|
51
|
-
## Usage Examples
|
|
52
|
-
|
|
53
|
-
### Complete Reverse Image Search
|
|
54
|
-
```
|
|
55
|
-
Can you do a complete reverse search on: https://copyseeker.net/assets/img/pc.png
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Copyright & Source Verification
|
|
59
|
-
```
|
|
60
|
-
I need to check if I can legally use this image. Please analyze: [image URL]
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Find Exact Matches
|
|
64
|
-
```
|
|
65
|
-
Where exactly does this image appear online? [image URL]
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Find Design Inspiration
|
|
69
|
-
```
|
|
70
|
-
Show me images that look similar to this design for inspiration: [image URL]
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Page Authority Assessment
|
|
74
|
-
```
|
|
75
|
-
Which website is the most credible source for this image? [image URL]
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Available Tools
|
|
79
|
-
|
|
80
|
-
### `reverse_image_search`
|
|
81
|
-
Complete reverse image search with all results formatted clearly.
|
|
82
|
-
|
|
83
|
-
**Parameters:**
|
|
84
|
-
- `image_url` (required): Direct URL to the image
|
|
85
|
-
|
|
86
|
-
**Returns:**
|
|
87
|
-
- AI analysis of what the image represents
|
|
88
|
-
- Exact matches (websites containing the same image)
|
|
89
|
-
- Visually similar images (different images that look alike)
|
|
90
|
-
- Page authority scores for credibility assessment
|
|
91
|
-
- AI-detected categories with confidence scores
|
|
92
|
-
|
|
93
|
-
### `analyze_image_copyright`
|
|
94
|
-
Specialized copyright and usage rights analysis.
|
|
95
|
-
|
|
96
|
-
**Parameters:**
|
|
97
|
-
- `image_url` (required): URL of the image to analyze
|
|
98
|
-
|
|
99
|
-
**Returns:**
|
|
100
|
-
- Copyright risk assessment based on where image appears
|
|
101
|
-
- Source credibility ranking by page authority
|
|
102
|
-
- Usage recommendations and next steps
|
|
103
|
-
- Likely original source identification
|
|
44
|
+
### Cursor and other clients
|
|
104
45
|
|
|
105
|
-
|
|
106
|
-
Find websites containing the exact same image.
|
|
46
|
+
Use the same `command`, `args` and `env` as above in the client's MCP settings (Cursor: `~/.cursor/mcp.json`).
|
|
107
47
|
|
|
108
|
-
|
|
109
|
-
- `image_url` (required): URL of the image to find matches for
|
|
48
|
+
## Examples
|
|
110
49
|
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
- Direct URLs to image files on each site
|
|
115
|
-
- Summary of distribution patterns
|
|
50
|
+
- "Where else on the web does https://example.com/photo.jpg appear? Who published it first?"
|
|
51
|
+
- "Is ~/Downloads/portrait.png AI-generated?"
|
|
52
|
+
- "Check these five product images for NSFW content before I upload them."
|
|
116
53
|
|
|
117
|
-
|
|
118
|
-
Discover visually similar images for inspiration and alternatives.
|
|
54
|
+
## Errors
|
|
119
55
|
|
|
120
|
-
|
|
121
|
-
- `image_url` (required): URL of the image to find similarities for
|
|
56
|
+
Failures come back to the assistant as plain explanations it can act on: an invalid key, a missing plan, an empty balance (with the top-up link), the rate limit of 60 calls per minute (with how long to wait), or a URL that points to a web page instead of an image. A search that fails on Copyseeker's side is refunded.
|
|
122
57
|
|
|
123
|
-
|
|
124
|
-
- URLs to different images that look visually similar
|
|
125
|
-
- Common visual elements and themes
|
|
126
|
-
- Use cases for similar images
|
|
127
|
-
- Style and category analysis
|
|
58
|
+
## Upgrading from 1.x
|
|
128
59
|
|
|
129
|
-
|
|
60
|
+
Version 2 calls the Copyseeker API directly instead of RapidAPI:
|
|
130
61
|
|
|
131
|
-
|
|
62
|
+
- Replace `COPYSEEKER_RAPIDAPI_KEY` with `COPYSEEKER_API_KEY`, a key from your Copyseeker account.
|
|
63
|
+
- The tools are now `reverse_image_search`, `detect_ai_image` and `detect_nsfw`. `analyze_image_copyright`, `find_exact_matches` and `find_similar_images` are gone: `reverse_image_search` covers them.
|
|
64
|
+
- Node.js 20 or newer is required.
|
|
132
65
|
|
|
133
|
-
##
|
|
66
|
+
## Development
|
|
134
67
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Check your usage on the [RapidAPI dashboard](https://rapidapi.com/developer/dashboard) and consider upgrading your plan.
|
|
140
|
-
|
|
141
|
-
### "Image not found" error
|
|
142
|
-
Ensure the image URL is directly accessible and points to a valid image file (not a webpage containing an image).
|
|
143
|
-
|
|
144
|
-
## Support
|
|
68
|
+
```bash
|
|
69
|
+
npm install
|
|
70
|
+
npm test # builds, then runs the server against a fake Copyseeker API
|
|
71
|
+
```
|
|
145
72
|
|
|
146
|
-
|
|
147
|
-
- 📚 API Documentation: [RapidAPI Docs](https://rapidapi.com/copyseeker1-copyseeker-default/api/reverse-image-search-by-copyseeker/)
|
|
73
|
+
`COPYSEEKER_API_URL` points the server at another API base URL (default `https://api.copyseeker.net`).
|
|
148
74
|
|
|
149
75
|
## License
|
|
150
76
|
|
|
151
|
-
MIT
|
|
152
|
-
|
|
153
|
-
## Contributing
|
|
154
|
-
|
|
155
|
-
We welcome contributions! Please see our contributing guidelines and submit pull requests to our GitHub repository.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
**Made with ❤️ by the Copyseeker team**
|
|
77
|
+
MIT
|
package/build/client.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const DEFAULT_API_URL = "https://api.copyseeker.net";
|
|
5
|
+
export const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
// Reverse image search can take tens of seconds on images found on many pages.
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 120_000;
|
|
8
|
+
const MIME_TYPES = {
|
|
9
|
+
".jpg": "image/jpeg",
|
|
10
|
+
".jpeg": "image/jpeg",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".gif": "image/gif",
|
|
13
|
+
".webp": "image/webp",
|
|
14
|
+
".bmp": "image/bmp",
|
|
15
|
+
".tif": "image/tiff",
|
|
16
|
+
".tiff": "image/tiff",
|
|
17
|
+
".heic": "image/heic",
|
|
18
|
+
".heif": "image/heif",
|
|
19
|
+
".avif": "image/avif",
|
|
20
|
+
};
|
|
21
|
+
// A failure the assistant can explain to the user and often fix (new key, top-up, other image).
|
|
22
|
+
export class CopyseekerError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
constructor(message, code) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.name = "CopyseekerError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class CopyseekerClient {
|
|
31
|
+
apiKey;
|
|
32
|
+
apiUrl;
|
|
33
|
+
fetchImpl;
|
|
34
|
+
constructor(apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch) {
|
|
35
|
+
this.apiKey = apiKey;
|
|
36
|
+
this.apiUrl = apiUrl;
|
|
37
|
+
this.fetchImpl = fetchImpl;
|
|
38
|
+
}
|
|
39
|
+
async call(endpoint, image) {
|
|
40
|
+
const source = image.trim();
|
|
41
|
+
const headers = { "X-API-Key": this.apiKey, Accept: "application/json" };
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
if (isHttpUrl(source)) {
|
|
45
|
+
const url = `${this.apiUrl}/${endpoint}?imageUrl=${encodeURIComponent(source)}`;
|
|
46
|
+
response = await this.fetchImpl(url, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const body = new FormData();
|
|
50
|
+
const file = await readImageFile(source);
|
|
51
|
+
body.append("file", new Blob([file.bytes], { type: file.mimeType }), file.name);
|
|
52
|
+
response = await this.fetchImpl(`${this.apiUrl}/${endpoint}`, { method: "POST", headers, body, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof CopyseekerError) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
60
|
+
throw new CopyseekerError(`Copyseeker did not answer within ${REQUEST_TIMEOUT_MS / 1000} seconds. Try again in a moment.`, "TIMEOUT");
|
|
61
|
+
}
|
|
62
|
+
throw new CopyseekerError(`Could not reach Copyseeker (${error instanceof Error ? error.message : String(error)}). Check the internet connection.`, "NETWORK_ERROR");
|
|
63
|
+
}
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
return (await response.json());
|
|
66
|
+
}
|
|
67
|
+
throw await toError(response);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function isHttpUrl(value) {
|
|
71
|
+
return /^https?:\/\//i.test(value);
|
|
72
|
+
}
|
|
73
|
+
async function readImageFile(source) {
|
|
74
|
+
const path = source.toLowerCase().startsWith("file://") ? fileURLToPath(source) : source;
|
|
75
|
+
let size;
|
|
76
|
+
try {
|
|
77
|
+
const info = await stat(path);
|
|
78
|
+
if (!info.isFile()) {
|
|
79
|
+
throw new CopyseekerError(`"${source}" is not a file. Pass a direct image URL (http/https) or the path to a local image file.`, "INVALID_IMAGE");
|
|
80
|
+
}
|
|
81
|
+
size = info.size;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof CopyseekerError) {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
throw new CopyseekerError(`"${source}" is neither an http(s) image URL nor a readable local file.`, "INVALID_IMAGE");
|
|
88
|
+
}
|
|
89
|
+
if (size === 0 || size > MAX_FILE_BYTES) {
|
|
90
|
+
throw new CopyseekerError(`"${source}" is ${size === 0 ? "empty" : "larger than 20 MB"}. Copyseeker accepts images up to 20 MB.`, "INVALID_IMAGE");
|
|
91
|
+
}
|
|
92
|
+
const name = basename(path);
|
|
93
|
+
return { bytes: new Uint8Array(await readFile(path)), name, mimeType: MIME_TYPES[extname(name).toLowerCase()] ?? "application/octet-stream" };
|
|
94
|
+
}
|
|
95
|
+
async function toError(response) {
|
|
96
|
+
let code = "";
|
|
97
|
+
try {
|
|
98
|
+
code = (await response.json())?.error ?? "";
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Not JSON (a gateway error page); the status code still tells what happened.
|
|
102
|
+
}
|
|
103
|
+
switch (code) {
|
|
104
|
+
case "INVALID_API_KEY":
|
|
105
|
+
return new CopyseekerError("The Copyseeker API key is missing or invalid. Create one at https://copyseeker.net/account#api and set it as COPYSEEKER_API_KEY in the MCP server config.", code);
|
|
106
|
+
case "PLAN_REQUIRED":
|
|
107
|
+
return new CopyseekerError("API access needs a paid Copyseeker plan. See https://copyseeker.net/pricing.", code);
|
|
108
|
+
case "QUOTA_EXHAUSTED":
|
|
109
|
+
return new CopyseekerError("The Copyseeker account has no searches left for this call. Top up at https://copyseeker.net/account#searches or wait for the monthly reset.", code);
|
|
110
|
+
case "RATE_LIMITED": {
|
|
111
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
112
|
+
return new CopyseekerError(`Copyseeker's rate limit (60 calls per minute) was hit. Retry after ${retryAfter ?? "a few"} seconds.`, code);
|
|
113
|
+
}
|
|
114
|
+
case "INVALID_IMAGE":
|
|
115
|
+
return new CopyseekerError("Copyseeker could not read an image there. Use a direct link to the image file itself (not a web page that shows it), or a local file path. Nothing was charged.", code);
|
|
116
|
+
case "SEARCH_FAILED":
|
|
117
|
+
return new CopyseekerError("The search failed on Copyseeker's side and its searches were refunded. Try again.", code);
|
|
118
|
+
default:
|
|
119
|
+
return new CopyseekerError(`Copyseeker answered HTTP ${response.status}${code ? ` (${code})` : ""}.`, code || `HTTP_${response.status}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
|
+
import { CopyseekerClient, DEFAULT_API_URL } from "./client.js";
|
|
4
|
+
import { createServer } from "./server.js";
|
|
5
|
+
const apiKey = process.env.COPYSEEKER_API_KEY?.trim();
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
// stdout carries the protocol, so everything for people goes to stderr.
|
|
8
|
+
console.error("COPYSEEKER_API_KEY is not set. Create an API key at https://copyseeker.net/account#api and add it to this server's env in your MCP client config.");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const client = new CopyseekerClient(apiKey, process.env.COPYSEEKER_API_URL?.trim() || DEFAULT_API_URL);
|
|
12
|
+
// One factory serves both clients that open with the 2025 initialize handshake and stateless 2026-07-28 clients.
|
|
13
|
+
serveStdio(() => createServer(client), {
|
|
14
|
+
onerror: (error) => {
|
|
15
|
+
console.error(`[copyseeker-mcp] ${error.message}`);
|
|
16
|
+
},
|
|
17
|
+
});
|
package/build/server.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CopyseekerError, } from "./client.js";
|
|
4
|
+
export const SERVER_NAME = "copyseeker";
|
|
5
|
+
export const SERVER_VERSION = "2.0.0";
|
|
6
|
+
const DEFAULT_MAX_RESULTS = 25;
|
|
7
|
+
const INSTRUCTIONS = `Copyseeker tools work on one image each, given as a direct image URL (http/https) or the path to a local image file.
|
|
8
|
+
Each call is charged to the user's Copyseeker searches: reverse_image_search 1, detect_ai_image 0.01, detect_nsfw 0.01.
|
|
9
|
+
Call a tool once per image and question; do not repeat a call that already succeeded.`;
|
|
10
|
+
const imageInput = z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Direct URL of the image file (http or https, not the web page showing it), or the absolute path to a local image file up to 20 MB.");
|
|
14
|
+
const SEARCH_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
15
|
+
export function createServer(client) {
|
|
16
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
|
|
17
|
+
server.registerTool("reverse_image_search", {
|
|
18
|
+
title: "Reverse image search",
|
|
19
|
+
description: "Find the web pages that show this image, with the copies of it each page uses, ranked by how prominent the page is. " +
|
|
20
|
+
"Use it to find where an image came from, who published it first, or where it is reused without permission. Costs 1 search.",
|
|
21
|
+
inputSchema: z.object({
|
|
22
|
+
image: imageInput,
|
|
23
|
+
max_results: z.number().int().min(1).max(100).default(DEFAULT_MAX_RESULTS).describe("Most pages to return, best ranked first."),
|
|
24
|
+
}),
|
|
25
|
+
outputSchema: z.object({
|
|
26
|
+
totalPages: z.number().int().describe("Pages Copyseeker found, before max_results."),
|
|
27
|
+
pages: z.array(z.object({
|
|
28
|
+
url: z.string(),
|
|
29
|
+
title: z.string().nullable(),
|
|
30
|
+
rank: z.number().describe("Page prominence; lower is more prominent."),
|
|
31
|
+
matchingImages: z.array(z.string()).describe("URLs of the copies of the image on this page."),
|
|
32
|
+
})),
|
|
33
|
+
}),
|
|
34
|
+
annotations: { ...SEARCH_ANNOTATIONS, title: "Reverse image search" },
|
|
35
|
+
}, async ({ image, max_results }) => {
|
|
36
|
+
return run(async () => {
|
|
37
|
+
const result = await client.call("reverse-image-search", image);
|
|
38
|
+
const pages = result.pages ?? [];
|
|
39
|
+
return { totalPages: pages.length, pages: pages.slice(0, max_results) };
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
server.registerTool("detect_ai_image", {
|
|
43
|
+
title: "Detect AI-generated image",
|
|
44
|
+
description: "Tell whether an image was generated by AI (Midjourney, DALL-E, Stable Diffusion, Flux and similar) or is a real photo or artwork. Costs 0.01 searches.",
|
|
45
|
+
inputSchema: z.object({ image: imageInput }),
|
|
46
|
+
outputSchema: z.object({
|
|
47
|
+
isAiGenerated: z.boolean(),
|
|
48
|
+
confidence: z.number().min(0).max(1).describe("How sure the verdict is, from 0 to 1."),
|
|
49
|
+
}),
|
|
50
|
+
annotations: { ...SEARCH_ANNOTATIONS, title: "Detect AI-generated image" },
|
|
51
|
+
}, async ({ image }) => {
|
|
52
|
+
return run(async () => {
|
|
53
|
+
const result = await client.call("ai-detection", image);
|
|
54
|
+
return { isAiGenerated: result.isAiGenerated, confidence: result.confidence };
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
server.registerTool("detect_nsfw", {
|
|
58
|
+
title: "Detect NSFW image",
|
|
59
|
+
description: "Tell whether an image contains nudity or sexual content that is not safe for work. Costs 0.01 searches.",
|
|
60
|
+
inputSchema: z.object({ image: imageInput }),
|
|
61
|
+
outputSchema: z.object({
|
|
62
|
+
isNsfw: z.boolean(),
|
|
63
|
+
confidence: z.number().min(0).max(1).describe("How sure the verdict is, from 0 to 1."),
|
|
64
|
+
}),
|
|
65
|
+
annotations: { ...SEARCH_ANNOTATIONS, title: "Detect NSFW image" },
|
|
66
|
+
}, async ({ image }) => {
|
|
67
|
+
return run(async () => {
|
|
68
|
+
const result = await client.call("nsfw-detection", image);
|
|
69
|
+
return { isNsfw: result.isNsfw, confidence: result.confidence };
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return server;
|
|
73
|
+
}
|
|
74
|
+
// Copyseeker failures go back to the model as tool errors it can read and act on, not as protocol errors.
|
|
75
|
+
async function run(call) {
|
|
76
|
+
try {
|
|
77
|
+
const output = await call();
|
|
78
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }], structuredContent: output };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const message = error instanceof CopyseekerError ? error.message : `Unexpected error: ${error instanceof Error ? error.message : String(error)}`;
|
|
82
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
]
|
|
2
|
+
"name": "copyseeker-mcp-server",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP server for Copyseeker: reverse image search, AI image detection and NSFW detection for AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"copyseeker-mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.build.json",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "npm run build && node --test --import tsx \"test/**/*.test.ts\"",
|
|
13
|
+
"start": "node build/index.js",
|
|
14
|
+
"prepublishOnly": "npm test"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/server": "^2.4.0",
|
|
18
|
+
"zod": "^4.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@modelcontextprotocol/client": "^2.4.0",
|
|
22
|
+
"@types/node": "^20.19.0",
|
|
23
|
+
"tsx": "^4.23.0",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mcp",
|
|
28
|
+
"model-context-protocol",
|
|
29
|
+
"copyseeker",
|
|
30
|
+
"reverse-image-search",
|
|
31
|
+
"ai-image-detection",
|
|
32
|
+
"nsfw-detection",
|
|
33
|
+
"image-search",
|
|
34
|
+
"copyright"
|
|
35
|
+
],
|
|
36
|
+
"author": "Copyseeker <support@copyseeker.net>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"homepage": "https://copyseeker.net/apis",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/copyseeker/copyseeker-mcp-server.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/copyseeker/copyseeker-mcp-server/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"build/**/*",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
]
|
|
55
54
|
}
|