Add MCP server for AI agents
Expose webshare_search, webshare_get_link, and webshare_login_check over stdio via the Model Context Protocol SDK. Document client configuration.
This commit is contained in:
153
src/mcp-server.js
Normal file
153
src/mcp-server.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
||||
const { z } = require('zod');
|
||||
const { WebshareClient } = require('./webshare');
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let n = bytes;
|
||||
while (n >= 1024 && i < units.length - 1) {
|
||||
n /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${n.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an MCP server that exposes Webshare search and link resolution tools.
|
||||
* @param {{ username: string, password: string }} credentials
|
||||
*/
|
||||
function createWebshareMcpServer({ username, password }) {
|
||||
const client = new WebshareClient(username, password);
|
||||
const server = new McpServer({
|
||||
name: 'webshare-api',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
'webshare_search',
|
||||
{
|
||||
title: 'Search Webshare',
|
||||
description:
|
||||
'Search Webshare.cz for files (default category: video). Returns ident, name, size, and votes for each hit. Use ident with webshare_get_link to resolve a download URL.',
|
||||
inputSchema: {
|
||||
query: z.string().min(1).describe('Search query (title, SxxExx, etc.)'),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.optional()
|
||||
.describe('Max results (1-100, default 25)'),
|
||||
offset: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe('Result offset for pagination (default 0)'),
|
||||
sort: z
|
||||
.enum(['rating', 'recent', 'largest', 'smallest'])
|
||||
.optional()
|
||||
.describe('Sort order (default rating)'),
|
||||
category: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Webshare category filter (default video)'),
|
||||
},
|
||||
},
|
||||
async ({ query, limit, offset, sort, category }) => {
|
||||
try {
|
||||
await client.ensureAuth();
|
||||
const results = await client.search(query, {
|
||||
limit: limit || 25,
|
||||
offset: offset || 0,
|
||||
sort: sort || 'rating',
|
||||
category: category || 'video',
|
||||
});
|
||||
|
||||
const lines = results.map(
|
||||
(f, i) =>
|
||||
`${i + 1}. ${f.name}\n ident: ${f.ident}\n size: ${formatSize(f.size)} (${f.size} bytes)\n votes: ${f.votes}`
|
||||
);
|
||||
const text =
|
||||
results.length === 0
|
||||
? `No results for query: ${query}`
|
||||
: `Found ${results.length} result(s) for "${query}":\n\n${lines.join('\n\n')}`;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text }],
|
||||
structuredContent: { query, count: results.length, results },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: `webshare_search failed: ${err.message}` }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'webshare_get_link',
|
||||
{
|
||||
title: 'Resolve Webshare download link',
|
||||
description:
|
||||
'Resolve a temporary HTTPS CDN download/stream URL for a Webshare file ident. Links expire after a short time (~10 minutes). Requires an authenticated Webshare account.',
|
||||
inputSchema: {
|
||||
ident: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Webshare file ident from webshare_search results'),
|
||||
},
|
||||
},
|
||||
async ({ ident }) => {
|
||||
try {
|
||||
const link = await client.getFileLink(ident);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Resolved link for ident ${ident} (expires soon):\n${link}`,
|
||||
},
|
||||
],
|
||||
structuredContent: { ident, link },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: `webshare_get_link failed: ${err.message}` }],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'webshare_login_check',
|
||||
{
|
||||
title: 'Check Webshare authentication',
|
||||
description:
|
||||
'Verify that WEBSHARE_USERNAME / WEBSHARE_PASSWORD work by performing a login against the Webshare API.',
|
||||
inputSchema: {},
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await client.ensureAuth();
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Webshare authentication OK.' }],
|
||||
structuredContent: { ok: true },
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: `Authentication failed: ${err.message}` }],
|
||||
structuredContent: { ok: false, error: err.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
module.exports = { createWebshareMcpServer, formatSize };
|
||||
32
src/mcp.js
Executable file
32
src/mcp.js
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP stdio entrypoint for AI agents.
|
||||
*
|
||||
* Configure clients with:
|
||||
* command: node
|
||||
* args: ["…/webshare-api/src/mcp.js"]
|
||||
* env: { WEBSHARE_USERNAME, WEBSHARE_PASSWORD }
|
||||
*
|
||||
* Do not write application logs to stdout — it is reserved for the MCP protocol.
|
||||
*/
|
||||
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
||||
const { createWebshareMcpServer } = require('./mcp-server');
|
||||
|
||||
async function main() {
|
||||
const username = process.env.WEBSHARE_USERNAME;
|
||||
const password = process.env.WEBSHARE_PASSWORD;
|
||||
if (!username || !password) {
|
||||
console.error('WEBSHARE_USERNAME and WEBSHARE_PASSWORD must be set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = createWebshareMcpServer({ username, password });
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('webshare-api MCP server running on stdio');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('MCP server failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -34,7 +34,8 @@ class WebshareClient {
|
||||
});
|
||||
if (r2.status !== 'OK') throw new Error(`login() failed: ${r2.message}`);
|
||||
this.wst = r2.token;
|
||||
console.log('Webshare: authenticated');
|
||||
// stderr only — MCP stdio must keep stdout clean
|
||||
console.error('Webshare: authenticated');
|
||||
}
|
||||
|
||||
async ensureAuth() {
|
||||
@@ -43,8 +44,8 @@ class WebshareClient {
|
||||
await this._loginPromise;
|
||||
}
|
||||
|
||||
async search(query, { limit = 30, offset = 0, sort = 'rating' } = {}) {
|
||||
const data = { what: query, category: 'video', sort, limit, offset };
|
||||
async search(query, { limit = 30, offset = 0, sort = 'rating', category = 'video' } = {}) {
|
||||
const data = { what: query, category, sort, limit, offset };
|
||||
if (this.wst) data.wst = this.wst;
|
||||
const r = await post('search', data);
|
||||
if (!r.file) return [];
|
||||
|
||||
Reference in New Issue
Block a user