@skvil/mcp-server 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Skvil
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,248 @@
1
+ <div align="center">
2
+
3
+ # skvil-mcp
4
+
5
+ **MCP server for the Skvil security scanner**
6
+
7
+ Verify, scan, and check on-chain certifications for AI agent skills — directly from your AI assistant.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@skvil/mcp-server)](https://www.npmjs.com/package/@skvil/mcp-server)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
11
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933)](https://nodejs.org)
12
+ [![MCP](https://img.shields.io/badge/MCP-compatible-8B5CF6)](https://modelcontextprotocol.io)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## Why skvil-mcp?
19
+
20
+ AI agents install skills from the internet — but how do you know a skill is safe?
21
+
22
+ Skvil is a community-powered security scanner that analyzes AI agent skills for malicious patterns, builds reputation scores through crowdsourced scans, and issues **on-chain certifications** that are tamper-proof and publicly verifiable.
23
+
24
+ This MCP server gives your AI agent native tools to interact with the Skvil network. No HTTP knowledge required — just ask your agent to verify a skill.
25
+
26
+ ### On-chain certification
27
+
28
+ Skvil's certification pipeline is what sets it apart — the entire process is **fully automated with zero human intervention**:
29
+
30
+ 1. **Community scanning** — multiple independent agents scan the same skill
31
+ 2. **Reputation building** — scores aggregate via exponential moving average (EMA)
32
+ 3. **Crucible analysis** — automated static analysis scans 32+ pattern categories, then an AI triage phase (embeddings + LLM) validates findings and filters false positives
33
+ 4. **On-chain registration** — skills scoring ≥ 80 are automatically anchored on Solana via SPL Memo transactions, creating a tamper-proof trust anchor that no single party can forge or revoke silently
34
+
35
+ Certification is algorithmic: score ≥ 50 passes, score < 50 fails and revokes any existing certificate. A periodic re-certification scheduler re-analyzes certified skills and revokes those that no longer pass.
36
+
37
+ When you run `skvil_verify`, you're not just checking a database — you're verifying against an immutable on-chain record.
38
+
39
+ ---
40
+
41
+ ## Quick start
42
+
43
+ ### Claude Desktop
44
+
45
+ Add to your `claude_desktop_config.json`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "skvil": {
51
+ "command": "npx",
52
+ "args": ["-y", "@skvil/mcp-server"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Claude Code
59
+
60
+ Add to your project's `.mcp.json`:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "skvil": {
66
+ "command": "npx",
67
+ "args": ["-y", "@skvil/mcp-server"]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### VS Code / Cursor
74
+
75
+ Add to your settings (JSON):
76
+
77
+ ```json
78
+ {
79
+ "mcp.servers": {
80
+ "skvil": {
81
+ "command": "npx",
82
+ "args": ["-y", "@skvil/mcp-server"]
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ That's it. The server auto-registers a free API key on first use. Zero config.
89
+
90
+ ---
91
+
92
+ ## Tools
93
+
94
+ | Tool | Auth | Description |
95
+ |------|------|-------------|
96
+ | `skvil_verify` | No | Check if a skill is safe by its SHA-256 hash. Returns reputation score, risk level, on-chain certification status, and Crucible behavioral analysis. |
97
+ | `skvil_stats` | No | Community statistics: total skills scanned, trusted, critical, and on-chain certified counts. |
98
+ | `skvil_certified` | No | List skills with active on-chain certifications (V1/V2/V3/Gold). Up to 10 most recent. |
99
+ | `skvil_register` | No | Get a free API key (500 scans/day). Auto-cached locally for future use. |
100
+ | `skvil_scan` | Key | Submit security scan results to the community reputation network. |
101
+ | `skvil_report` | Key | Report a suspicious skill. Confirmed reports trigger automatic on-chain revocation. |
102
+
103
+ ### Certification levels
104
+
105
+ | Level | Meaning |
106
+ |-------|---------|
107
+ | **V1** | Basic verification — scanned by community, passed automated static analysis (32+ pattern categories + AI triage) |
108
+ | **V2** | Enhanced verification — V1 + passed Crucible behavioral analysis in sandboxed environment |
109
+ | **V3** | Full verification — V2 + passed periodic re-certification cycles |
110
+ | **Gold** | Highest trust — V3 + continuous monitoring, reserved for critical infrastructure skills |
111
+
112
+ All levels are registered on-chain. The entire certification process is automated — no human review is involved at any level. Higher levels require progressively more rigorous automated verification.
113
+
114
+ ---
115
+
116
+ ## Configuration
117
+
118
+ ### API key
119
+
120
+ The server automatically registers a free API key on first use and caches it in `~/.skvil/mcp-config.json`.
121
+
122
+ To use an existing key:
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "skvil": {
128
+ "command": "npx",
129
+ "args": ["-y", "@skvil/mcp-server"],
130
+ "env": {
131
+ "SKVIL_API_KEY": "sk_your_key_here"
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ### Custom API URL
139
+
140
+ For self-hosted or development instances:
141
+
142
+ ```json
143
+ {
144
+ "env": {
145
+ "SKVIL_API_URL": "http://localhost:8000"
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Key resolution order
151
+
152
+ 1. `SKVIL_API_KEY` environment variable
153
+ 2. Cached key in `~/.skvil/mcp-config.json`
154
+ 3. Legacy Python client key in `~/.skvil/config`
155
+
156
+ ---
157
+
158
+ ## Examples
159
+
160
+ ### "Is this skill safe to install?"
161
+
162
+ ```
163
+ User: Check if sha256:4a2f...c81e is safe
164
+
165
+ Agent calls skvil_verify → gets reputation, certification, findings
166
+
167
+ Agent: This skill has a reputation score of 91.2 (safe) and is
168
+ on-chain certified at V2 level. 15 community scans with
169
+ no critical findings. Safe to install.
170
+ ```
171
+
172
+ ### "What skills are certified?"
173
+
174
+ ```
175
+ User: Show me certified skills
176
+
177
+ Agent calls skvil_certified → gets list of on-chain certified skills
178
+
179
+ Agent: There are 7 on-chain certified skills:
180
+ 1. web-scraper — V2, score 91.2, certified Feb 18
181
+ 2. data-pipeline — V1, score 85.0, certified Feb 15
182
+ ...
183
+ ```
184
+
185
+ ### "Report this suspicious skill"
186
+
187
+ ```
188
+ User: Report sha256:dead...beef — it's sending data to an unknown server
189
+
190
+ Agent calls skvil_report → submits report
191
+
192
+ Agent: Report #42 submitted. The skill will be re-analyzed
193
+ automatically. If the report is confirmed, the skill
194
+ will be flagged and any on-chain certification will
195
+ be revoked.
196
+ ```
197
+
198
+ ---
199
+
200
+ ## How it works
201
+
202
+ ```
203
+ ┌─────────────┐ stdio ┌────────────┐ HTTPS ┌──────────────────┐
204
+ │ AI Agent │ ◄────────────► │ skvil-mcp │ ────────────► │ api.skvil.com │
205
+ │ (Claude, │ MCP tools │ (local) │ REST API │ (reputation DB │
206
+ │ GPT, etc) │ │ │ │ + on-chain) │
207
+ └─────────────┘ └────────────┘ └──────────────────┘
208
+ ```
209
+
210
+ The MCP server runs locally as a subprocess of your AI client. It translates MCP tool calls into HTTPS requests to the Skvil API. No data is stored remotely except scan results and reports — and certifications are anchored on-chain for public verification.
211
+
212
+ ---
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ git clone https://github.com/Skvil-IA/skvil-mcp.git
218
+ cd skvil-mcp
219
+ npm install
220
+ npm run build
221
+ ```
222
+
223
+ ### Run locally
224
+
225
+ ```bash
226
+ # Point to local API for development
227
+ SKVIL_API_URL=http://localhost:8000 node dist/index.js
228
+ ```
229
+
230
+ ### Test with MCP Inspector
231
+
232
+ ```bash
233
+ npx @modelcontextprotocol/inspector node dist/index.js
234
+ ```
235
+
236
+ ### Lint & format
237
+
238
+ ```bash
239
+ npm run lint
240
+ npm run format
241
+ npm run typecheck
242
+ ```
243
+
244
+ ---
245
+
246
+ ## License
247
+
248
+ [MIT](LICENSE) — Skvil 2026
package/dist/api.js ADDED
@@ -0,0 +1,123 @@
1
+ import { getApiKey, getBaseUrl, saveApiKey } from './config.js';
2
+ import { VERSION } from './version.js';
3
+ const USER_AGENT = `skvil-mcp/${VERSION}`;
4
+ const TIMEOUT_MS = 15_000;
5
+ const MAX_RESPONSE_BYTES = 1_048_576; // 1 MB
6
+ const RETRIABLE_STATUSES = new Set([502, 503, 504]);
7
+ const MAX_RETRIES = 2;
8
+ const BASE_DELAY_MS = 1_000;
9
+ export class SkvilApiError extends Error {
10
+ status;
11
+ detail;
12
+ constructor(status, detail) {
13
+ super(`HTTP ${status}: ${detail}`);
14
+ this.status = status;
15
+ this.detail = detail;
16
+ this.name = 'SkvilApiError';
17
+ }
18
+ }
19
+ async function request(method, path, options = {}) {
20
+ const url = `${getBaseUrl()}${path}`;
21
+ const headers = {
22
+ 'User-Agent': USER_AGENT,
23
+ Accept: 'application/json',
24
+ };
25
+ if (options.body !== undefined) {
26
+ headers['Content-Type'] = 'application/json';
27
+ }
28
+ if (options.auth) {
29
+ const key = getApiKey();
30
+ if (!key) {
31
+ throw new SkvilApiError(401, 'No API key configured. Use skvil_register to get one, or set SKVIL_API_KEY env var.');
32
+ }
33
+ headers['X-API-Key'] = key;
34
+ }
35
+ let lastError;
36
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
37
+ if (attempt > 0) {
38
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
39
+ await new Promise((resolve) => setTimeout(resolve, delay));
40
+ }
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
43
+ try {
44
+ const response = await fetch(url, {
45
+ method,
46
+ headers,
47
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
48
+ signal: controller.signal,
49
+ });
50
+ // Guard against excessively large responses
51
+ const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
52
+ if (contentLength > MAX_RESPONSE_BYTES) {
53
+ throw new SkvilApiError(502, 'Response too large');
54
+ }
55
+ const text = await response.text();
56
+ if (!response.ok) {
57
+ // Retry on transient server errors (not on 4xx client errors)
58
+ if (RETRIABLE_STATUSES.has(response.status) && attempt < MAX_RETRIES) {
59
+ lastError = new SkvilApiError(response.status, text.slice(0, 200));
60
+ continue;
61
+ }
62
+ let detail = text.slice(0, 500);
63
+ try {
64
+ const parsed = JSON.parse(text);
65
+ detail = parsed.detail || detail;
66
+ }
67
+ catch {
68
+ // Use raw text
69
+ }
70
+ throw new SkvilApiError(response.status, detail);
71
+ }
72
+ return JSON.parse(text);
73
+ }
74
+ catch (error) {
75
+ // Retry on network errors (AbortError = timeout, TypeError = DNS/connection)
76
+ if (error instanceof Error &&
77
+ (error.name === 'AbortError' || error.name === 'TypeError') &&
78
+ attempt < MAX_RETRIES) {
79
+ lastError = error;
80
+ continue;
81
+ }
82
+ throw error;
83
+ }
84
+ finally {
85
+ clearTimeout(timer);
86
+ }
87
+ }
88
+ // All retries exhausted
89
+ throw lastError;
90
+ }
91
+ /** Check if a skill is safe by its composite hash. */
92
+ export async function verify(hash) {
93
+ return request('GET', `/verify/${encodeURIComponent(hash)}`);
94
+ }
95
+ /** Get community statistics. */
96
+ export async function stats() {
97
+ return request('GET', '/stats');
98
+ }
99
+ /** List actively certified skills. */
100
+ export async function certified() {
101
+ return request('GET', '/certified');
102
+ }
103
+ /** List all certified skills with full catalog metadata. */
104
+ export async function catalog() {
105
+ return request('GET', '/catalog');
106
+ }
107
+ /** Register for a free API key and cache it locally. */
108
+ export async function register() {
109
+ const result = await request('POST', '/register');
110
+ saveApiKey(result.api_key, result.key_prefix);
111
+ return result;
112
+ }
113
+ /** Submit scan results for a skill. */
114
+ export async function scan(payload) {
115
+ return request('POST', '/scan', { body: payload, auth: true });
116
+ }
117
+ /** Report a suspicious skill. */
118
+ export async function report(hash, reason, details) {
119
+ const body = { composite_hash: hash, reason };
120
+ if (details !== undefined)
121
+ body.details = details;
122
+ return request('POST', '/report', { body, auth: true });
123
+ }
package/dist/config.js ADDED
@@ -0,0 +1,102 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const CONFIG_DIR = join(homedir(), '.skvil');
5
+ const CONFIG_FILE = join(CONFIG_DIR, 'mcp-config.json');
6
+ /** In-memory cache to avoid repeated disk reads. */
7
+ let cachedApiKey;
8
+ let cachedBaseUrl;
9
+ function readConfig() {
10
+ try {
11
+ if (!existsSync(CONFIG_FILE))
12
+ return {};
13
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ function writeConfig(config) {
21
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
22
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
23
+ }
24
+ /**
25
+ * Resolve the API key from (in priority order):
26
+ * 1. SKVIL_API_KEY or SKVIL_KEDAVRA_API_KEY environment variable
27
+ * 2. Cached key in ~/.skvil/mcp-config.json
28
+ * 3. Legacy Python client config at ~/.skvil/config
29
+ *
30
+ * Result is memoized for the lifetime of the process.
31
+ */
32
+ export function getApiKey() {
33
+ if (cachedApiKey !== undefined)
34
+ return cachedApiKey;
35
+ const envKey = process.env.SKVIL_API_KEY || process.env.SKVIL_KEDAVRA_API_KEY;
36
+ if (envKey) {
37
+ cachedApiKey = envKey;
38
+ return cachedApiKey;
39
+ }
40
+ const config = readConfig();
41
+ if (config.api_key) {
42
+ cachedApiKey = config.api_key;
43
+ return cachedApiKey;
44
+ }
45
+ // Try legacy Python client config (key=value format)
46
+ try {
47
+ const legacyPath = join(homedir(), '.skvil', 'config');
48
+ if (existsSync(legacyPath)) {
49
+ const content = readFileSync(legacyPath, 'utf-8');
50
+ const match = content.match(/^api_key\s*=\s*(.+)$/m);
51
+ if (match) {
52
+ const key = match[1].trim();
53
+ if (/^[a-zA-Z0-9_-]+$/.test(key)) {
54
+ cachedApiKey = key;
55
+ return cachedApiKey;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Ignore read errors
62
+ }
63
+ cachedApiKey = null;
64
+ return null;
65
+ }
66
+ /** Cache a newly registered API key for future use. */
67
+ export function saveApiKey(apiKey, keyPrefix) {
68
+ const config = readConfig();
69
+ config.api_key = apiKey;
70
+ config.key_prefix = keyPrefix;
71
+ config.registered_at = new Date().toISOString();
72
+ writeConfig(config);
73
+ cachedApiKey = apiKey;
74
+ }
75
+ /** Clear the memoized API key (used after registration). */
76
+ export function clearApiKeyCache() {
77
+ cachedApiKey = undefined;
78
+ }
79
+ /**
80
+ * Get the API base URL (override with SKVIL_API_URL or SKVIL_KEDAVRA_API_URL).
81
+ * Enforces HTTPS for non-localhost URLs to prevent credential leakage.
82
+ * Result is memoized for the lifetime of the process.
83
+ */
84
+ export function getBaseUrl() {
85
+ if (cachedBaseUrl !== undefined)
86
+ return cachedBaseUrl;
87
+ const override = process.env.SKVIL_API_URL || process.env.SKVIL_KEDAVRA_API_URL;
88
+ if (override) {
89
+ const isLocalhost = /^https?:\/\/(localhost|127\.0\.0\.1|::1)(:\d+)?/.test(override);
90
+ if (!override.startsWith('https://') && !isLocalhost) {
91
+ process.stderr.write('[skvil-mcp] WARNING: SKVIL_API_URL must use HTTPS. Falling back to api.skvil.com.\n');
92
+ cachedBaseUrl = 'https://api.skvil.com';
93
+ }
94
+ else {
95
+ cachedBaseUrl = override.replace(/\/+$/, '');
96
+ }
97
+ }
98
+ else {
99
+ cachedBaseUrl = 'https://api.skvil.com';
100
+ }
101
+ return cachedBaseUrl;
102
+ }
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { registerTools } from './tools.js';
5
+ import { VERSION } from './version.js';
6
+ process.on('unhandledRejection', (err) => {
7
+ process.stderr.write(`[skvil-mcp] unhandled rejection: ${err}\n`);
8
+ process.exit(1);
9
+ });
10
+ const server = new McpServer({
11
+ name: 'skvil',
12
+ version: VERSION,
13
+ });
14
+ registerTools(server);
15
+ try {
16
+ const transport = new StdioServerTransport();
17
+ await server.connect(transport);
18
+ const shutdown = async () => {
19
+ await server.close();
20
+ process.exit(0);
21
+ };
22
+ process.on('SIGINT', shutdown);
23
+ process.on('SIGTERM', shutdown);
24
+ }
25
+ catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ process.stderr.write(`[skvil-mcp] failed to start: ${message}\n`);
28
+ process.exit(1);
29
+ }
package/dist/tools.js ADDED
@@ -0,0 +1,351 @@
1
+ import { z } from 'zod';
2
+ import * as api from './api.js';
3
+ import { getApiKey } from './config.js';
4
+ const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
5
+ const hashSchema = z
6
+ .string()
7
+ .regex(HASH_PATTERN, 'Must be "sha256:" followed by 64 hex characters')
8
+ .describe('SHA-256 composite hash of the skill (e.g. "sha256:4a2f8b...c81e")');
9
+ function formatScore(score) {
10
+ if (score >= 80)
11
+ return `${score.toFixed(1)} (safe)`;
12
+ if (score >= 50)
13
+ return `${score.toFixed(1)} (caution)`;
14
+ return `${score.toFixed(1)} (danger)`;
15
+ }
16
+ /** Register all skvil tools on the MCP server. */
17
+ export function registerTools(server) {
18
+ // ── skvil_verify ──────────────────────────────────────────────────────────
19
+ server.tool('skvil_verify', 'Check if an AI agent skill is safe before installing it. Returns reputation ' +
20
+ 'score, risk level, certification status, and community scan data. ' +
21
+ 'Use this to verify any skill by its SHA-256 composite hash.', { hash: hashSchema }, async ({ hash }) => {
22
+ try {
23
+ const result = await api.verify(hash);
24
+ if (!result.known) {
25
+ return {
26
+ content: [
27
+ {
28
+ type: 'text',
29
+ text: `**Unknown skill** (${hash})\n\n` +
30
+ 'This skill has never been scanned by the Skvil network.\n' +
31
+ 'It has no reputation data or certification.\n\n' +
32
+ '**Recommendation:** Do not install without scanning first.',
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ const score = result.reputation_score ?? 0;
38
+ const totalScans = result.total_scans ?? 0;
39
+ const lines = [
40
+ `**Skill verification: ${hash}**\n`,
41
+ `- **Reputation score:** ${formatScore(score)}`,
42
+ `- **Total community scans:** ${totalScans}`,
43
+ `- **Risk level:** ${result.risk_summary?.last_risk_level ?? 'unknown'}`,
44
+ ];
45
+ if (result.certification) {
46
+ lines.push(`- **Certification:** ${result.certification}`);
47
+ }
48
+ else {
49
+ lines.push('- **Certification:** none');
50
+ }
51
+ if (result.confirmed_malicious) {
52
+ lines.push('\n**CONFIRMED MALICIOUS** — a Skvil admin has verified this skill is dangerous.');
53
+ lines.push('Do NOT install this skill.');
54
+ }
55
+ if (result.risk_summary) {
56
+ const f = result.risk_summary.findings_by_severity;
57
+ const critical = f.critical ?? 0;
58
+ const high = f.high ?? 0;
59
+ const medium = f.medium ?? 0;
60
+ const low = f.low ?? 0;
61
+ if (critical > 0 || high > 0) {
62
+ lines.push(`\n**Findings:** ${critical} critical, ${high} high, ${medium} medium, ${low} low`);
63
+ }
64
+ }
65
+ if (result.crucible) {
66
+ lines.push(`\n**Crucible behavioral analysis:** ${result.crucible.status}`);
67
+ lines.push(`- Behavioral score: ${result.crucible.score}`);
68
+ if (result.crucible.behavioral_findings.length > 0) {
69
+ const descriptions = result.crucible.behavioral_findings
70
+ .map((f) => f.description)
71
+ .join(', ');
72
+ lines.push(`- Findings: ${descriptions}`);
73
+ }
74
+ }
75
+ // Decision recommendation
76
+ lines.push('\n**Recommendation:**');
77
+ if (result.confirmed_malicious) {
78
+ lines.push('Do NOT install. This skill has been confirmed malicious.');
79
+ }
80
+ else if (result.crucible?.status === 'malicious') {
81
+ lines.push('Do NOT install. Behavioral analysis detected malicious activity.');
82
+ }
83
+ else if (score >= 80 && result.certification) {
84
+ lines.push(`Safe to install. This skill is certified (${result.certification}) ` +
85
+ 'with a strong reputation score.');
86
+ }
87
+ else if (score >= 60) {
88
+ lines.push('Likely safe, but not yet certified. Install with caution.');
89
+ }
90
+ else if (score < 40) {
91
+ lines.push('Do NOT install. Low reputation score indicates potential risk.');
92
+ }
93
+ else {
94
+ lines.push('Proceed with caution. Review findings before installing.');
95
+ }
96
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
97
+ }
98
+ catch (error) {
99
+ return { content: [{ type: 'text', text: formatError('verify', error) }], isError: true };
100
+ }
101
+ });
102
+ // ── skvil_stats ───────────────────────────────────────────────────────────
103
+ server.tool('skvil_stats', 'Get aggregate statistics from the Skvil community network: total skills ' +
104
+ 'scanned, trusted count, critical findings, and certified skills.', {}, async () => {
105
+ try {
106
+ const result = await api.stats();
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: '**Skvil community statistics**\n\n' +
112
+ `- **Total skills scanned:** ${result.total}\n` +
113
+ `- **Trusted** (reputation >= 70): ${result.trusted}\n` +
114
+ `- **Critical findings:** ${result.critical}\n` +
115
+ `- **Certified:** ${result.certified}`,
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ catch (error) {
121
+ return { content: [{ type: 'text', text: formatError('stats', error) }], isError: true };
122
+ }
123
+ });
124
+ // ── skvil_certified ───────────────────────────────────────────────────────
125
+ server.tool('skvil_certified', 'List skills that have been verified and certified by Skvil admins. ' +
126
+ 'Certified skills have been manually reviewed and registered for ' +
127
+ 'tamper-proof verification. Returns up to 10 most recently certified ' +
128
+ 'skills with their level (V1/V2/V3/Gold), reputation score, and ' +
129
+ 'certification date.', {}, async () => {
130
+ try {
131
+ const result = await api.certified();
132
+ if (result.length === 0) {
133
+ return {
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: 'No skills are currently certified. Be the first to get certified!',
138
+ },
139
+ ],
140
+ };
141
+ }
142
+ const lines = ['**Certified skills**\n'];
143
+ for (const skill of result) {
144
+ lines.push(`- **${skill.name}** — ${skill.level} | Score: ${formatScore(skill.reputation_score)} | ` +
145
+ `${skill.total_scans} scans | Certified: ${skill.certified_at}\n` +
146
+ ` Hash: \`${skill.composite_hash}\``);
147
+ }
148
+ lines.push('\nAll certifications are registered for tamper-proof, publicly verifiable trust.');
149
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
150
+ }
151
+ catch (error) {
152
+ return {
153
+ content: [{ type: 'text', text: formatError('certified', error) }],
154
+ isError: true,
155
+ };
156
+ }
157
+ });
158
+ // ── skvil_catalog ──────────────────────────────────────────────────────────
159
+ server.tool('skvil_catalog', 'Browse the full catalog of Skvil-certified AI agent skills with detailed ' +
160
+ 'metadata: author, version, description, provider, agent platform, file ' +
161
+ 'count, and install URL. Returns up to 100 skills. Use this to discover ' +
162
+ 'safe skills available for installation.', {}, async () => {
163
+ try {
164
+ const result = await api.catalog();
165
+ if (result.length === 0) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: 'The skill catalog is empty. No certified skills available yet.',
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ const lines = [`**Skvil skill catalog** (${result.length} certified skills)\n`];
176
+ for (const skill of result) {
177
+ const parts = [`- **${skill.name}**`];
178
+ parts.push(` Level: ${skill.level} | Score: ${formatScore(skill.reputation_score)} | ${skill.total_scans} scans`);
179
+ if (skill.author)
180
+ parts.push(` Author: ${skill.author}`);
181
+ if (skill.version)
182
+ parts.push(` Version: ${skill.version}`);
183
+ if (skill.description)
184
+ parts.push(` ${skill.description}`);
185
+ if (skill.provider)
186
+ parts.push(` Provider: ${skill.provider}`);
187
+ if (skill.agent)
188
+ parts.push(` Agent: ${skill.agent}`);
189
+ parts.push(` Files: ${skill.file_count} | Certified: ${skill.certified_at}`);
190
+ if (skill.skill_url)
191
+ parts.push(` Install: ${skill.skill_url}`);
192
+ parts.push(` Hash: \`${skill.composite_hash}\``);
193
+ lines.push(parts.join('\n'));
194
+ }
195
+ return { content: [{ type: 'text', text: lines.join('\n\n') }] };
196
+ }
197
+ catch (error) {
198
+ return {
199
+ content: [{ type: 'text', text: formatError('catalog', error) }],
200
+ isError: true,
201
+ };
202
+ }
203
+ });
204
+ // ── skvil_register ────────────────────────────────────────────────────────
205
+ server.tool('skvil_register', 'Register for a free Skvil API key. The key is automatically cached ' +
206
+ 'locally for future use. No sign-up or account required. Other tools ' +
207
+ '(skvil_scan, skvil_report) will use the cached key automatically.', {}, async () => {
208
+ try {
209
+ const existing = getApiKey();
210
+ if (existing) {
211
+ return {
212
+ content: [
213
+ {
214
+ type: 'text',
215
+ text: 'An API key is already configured.\n\n' +
216
+ 'To register a new key, unset the `SKVIL_API_KEY` env var and ' +
217
+ 'delete `~/.skvil/mcp-config.json`, then try again.',
218
+ },
219
+ ],
220
+ };
221
+ }
222
+ const result = await api.register();
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: '**API key registered successfully!**\n\n' +
228
+ `- **Key prefix:** ${result.key_prefix}...\n` +
229
+ `- **Tier:** ${result.tier}\n\n` +
230
+ 'The key has been cached in `~/.skvil/mcp-config.json`.\n' +
231
+ 'You can now use `skvil_scan` and `skvil_report`.',
232
+ },
233
+ ],
234
+ };
235
+ }
236
+ catch (error) {
237
+ return {
238
+ content: [{ type: 'text', text: formatError('register', error) }],
239
+ isError: true,
240
+ };
241
+ }
242
+ });
243
+ // ── skvil_report ──────────────────────────────────────────────────────────
244
+ server.tool('skvil_report', 'Report a suspicious or malicious AI agent skill to Skvil admins for review. ' +
245
+ 'Requires an API key (use skvil_register first). Reports are reviewed by ' +
246
+ 'admins and confirmed findings lead to certification revocation.', {
247
+ hash: hashSchema,
248
+ reason: z
249
+ .string()
250
+ .min(10)
251
+ .max(1000)
252
+ .describe('Why this skill is suspicious (10-1000 characters)'),
253
+ details: z
254
+ .string()
255
+ .max(5000)
256
+ .optional()
257
+ .describe('Additional details or evidence (optional, max 5000 characters)'),
258
+ }, async ({ hash, reason, details }) => {
259
+ try {
260
+ const result = await api.report(hash, reason, details);
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'text',
265
+ text: '**Report submitted successfully**\n\n' +
266
+ `- **Report ID:** ${result.report_id}\n` +
267
+ `- **Status:** ${result.status}\n` +
268
+ `- **Skill hash:** ${hash}\n\n` +
269
+ 'A Skvil admin will review this report. If confirmed, the skill ' +
270
+ 'will be flagged as malicious and any existing certification ' +
271
+ 'will be revoked.',
272
+ },
273
+ ],
274
+ };
275
+ }
276
+ catch (error) {
277
+ return { content: [{ type: 'text', text: formatError('report', error) }], isError: true };
278
+ }
279
+ });
280
+ // ── skvil_scan ────────────────────────────────────────────────────────────
281
+ server.tool('skvil_scan', 'Submit security scan results for an AI agent skill to the Skvil reputation ' +
282
+ 'network. Contributes to the community reputation score (EMA). Requires an ' +
283
+ 'API key (use skvil_register first). The server recomputes the score from ' +
284
+ 'findings — always provide accurate findings.', {
285
+ name: z.string().max(256).describe('Skill name'),
286
+ composite_hash: hashSchema,
287
+ file_count: z.number().int().min(0).max(10000).describe('Number of files in the skill'),
288
+ file_hashes: z
289
+ .record(z
290
+ .string()
291
+ .max(500)
292
+ .regex(/^[a-zA-Z0-9_\-./]+$/, 'Invalid file path'), z.string().regex(/^[a-f0-9]{64}$/, 'Must be 64 hex characters'))
293
+ .describe('Map of relative file paths to their SHA-256 hex hashes'),
294
+ score: z.number().int().min(0).max(100).describe('Computed security score (0-100)'),
295
+ risk_level: z.enum(['safe', 'caution', 'danger']).describe('Overall risk assessment'),
296
+ findings: z
297
+ .array(z.object({
298
+ severity: z.enum(['critical', 'high', 'medium', 'low']),
299
+ category: z.string().max(100),
300
+ description: z.string().max(1000),
301
+ file: z.string().max(500),
302
+ line: z.number().int().optional(),
303
+ }))
304
+ .max(500)
305
+ .default([])
306
+ .describe('Security findings detected in the skill'),
307
+ frontmatter: z
308
+ .record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
309
+ .optional()
310
+ .describe('SKILL.md frontmatter metadata (optional)'),
311
+ }, async (params) => {
312
+ try {
313
+ const result = await api.scan(params);
314
+ const lines = [
315
+ '**Scan submitted successfully**\n',
316
+ `- **Scan ID:** ${result.scan_id}`,
317
+ `- **Updated reputation:** ${formatScore(result.reputation_score)}`,
318
+ `- **Total community scans:** ${result.total_scans}`,
319
+ ];
320
+ if (result.certification) {
321
+ lines.push(`- **Certification:** ${result.certification}`);
322
+ }
323
+ lines.push('\nYour scan contributes to the community reputation via exponential ' +
324
+ 'moving average (EMA). Thank you for helping secure the AI skill ecosystem.');
325
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
326
+ }
327
+ catch (error) {
328
+ return { content: [{ type: 'text', text: formatError('scan', error) }], isError: true };
329
+ }
330
+ });
331
+ }
332
+ function formatError(tool, error) {
333
+ if (error instanceof api.SkvilApiError) {
334
+ if (error.status === 429) {
335
+ return `**Rate limit exceeded** — ${error.detail}\nTry again later.`;
336
+ }
337
+ if (error.status === 401) {
338
+ return (`**Authentication required** — ${error.detail}\n` +
339
+ 'Use `skvil_register` to get a free API key.');
340
+ }
341
+ const safeDetail = error.detail.slice(0, 500);
342
+ return `**Error in skvil_${tool}** (HTTP ${error.status})\n${safeDetail}`;
343
+ }
344
+ if (error instanceof Error) {
345
+ if (error.name === 'AbortError' || error.name === 'TimeoutError') {
346
+ return '**Timeout** — the Skvil API did not respond in time. Try again.';
347
+ }
348
+ return `**Error in skvil_${tool}**\n${error.message}`;
349
+ }
350
+ return `**Unexpected error in skvil_${tool}**\n${String(error)}`;
351
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ /** Skvil API response types. */
2
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Single source of truth for the package version. */
2
+ export const VERSION = '0.1.0';
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@skvil/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Skvil security scanner — verify, scan, and report AI agent skills",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "skvil-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/index.js",
17
+ "lint": "eslint src/",
18
+ "format": "prettier --write 'src/**/*.ts'",
19
+ "format:check": "prettier --check 'src/**/*.ts'",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "echo 'No tests configured yet' && exit 0",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "skvil",
31
+ "security",
32
+ "ai-agents",
33
+ "ai-skills",
34
+ "scanner"
35
+ ],
36
+ "author": "Skvil <terminal@skvil.com>",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/Skvil-IA/skvil-mcp.git"
41
+ },
42
+ "homepage": "https://skvil.com",
43
+ "bugs": {
44
+ "url": "https://github.com/Skvil-IA/skvil-mcp/issues"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.12.0",
51
+ "zod": "^3.24.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.0.0",
55
+ "typescript": "^5.7.0",
56
+ "eslint": "^9.0.0",
57
+ "@eslint/js": "^9.0.0",
58
+ "typescript-eslint": "^8.0.0",
59
+ "prettier": "^3.4.0"
60
+ }
61
+ }