@fidensa/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/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @fidensa/mcp-server
2
+
3
+ MCP server for [Fidensa](https://fidensa.com) — the independent AI capability certification authority.
4
+
5
+ Gives your AI agent structured access to Fidensa certification data through the Model Context Protocol. Check trust scores, search for certified alternatives, compare capabilities side-by-side, and verify signed artifacts — all through MCP tool calls.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx @fidensa/mcp-server
11
+ ```
12
+
13
+ Or install globally:
14
+
15
+ ```bash
16
+ npm install -g @fidensa/mcp-server
17
+ fidensa-mcp-server
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ | Variable | Required | Description |
23
+ |----------|----------|-------------|
24
+ | `FIDENSA_API_KEY` | No* | API key for full access. Get one free at [fidensa.com/docs/api](https://fidensa.com/docs/api) |
25
+ | `FIDENSA_BASE_URL` | No | Override API base URL (default: `https://fidensa.com`) |
26
+
27
+ \* `check_certification` and `search_capabilities` work without an API key. Other tools require a free Registered-tier key.
28
+
29
+ ## Tools
30
+
31
+ | Tool | Auth | Description |
32
+ |------|------|-------------|
33
+ | `check_certification` | None | Quick trust check — status, score, grade, tier |
34
+ | `search_capabilities` | None | Search for certified capabilities by keyword |
35
+ | `get_contract` | API key | Full certification contract with all evidence |
36
+ | `compare_capabilities` | API key | Side-by-side comparison of 2-5 capabilities |
37
+ | `verify_artifact` | API key | Verify cryptographic signatures on .cert.json artifacts |
38
+ | `report_experience` | — | Consumer experience reporting (coming soon) |
39
+
40
+ ## Agent Configuration
41
+
42
+ ### Claude Code
43
+
44
+ Add to your Claude Code MCP settings:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "fidensa": {
50
+ "command": "npx",
51
+ "args": ["@fidensa/mcp-server"],
52
+ "env": {
53
+ "FIDENSA_API_KEY": "fid_your_key_here"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Cursor
61
+
62
+ Add to `.cursor/mcp.json`:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "fidensa": {
68
+ "command": "npx",
69
+ "args": ["@fidensa/mcp-server"],
70
+ "env": {
71
+ "FIDENSA_API_KEY": "fid_your_key_here"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Windsurf / VS Code
79
+
80
+ Add to your MCP configuration:
81
+
82
+ ```json
83
+ {
84
+ "servers": {
85
+ "fidensa": {
86
+ "type": "stdio",
87
+ "command": "npx",
88
+ "args": ["@fidensa/mcp-server"],
89
+ "env": {
90
+ "FIDENSA_API_KEY": "fid_your_key_here"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## Example Agent Workflow
98
+
99
+ ```
100
+ User: "Set up a filesystem watcher that writes logs to SQLite."
101
+
102
+ Agent:
103
+ 1. check_certification("mcp-server-filesystem")
104
+ → Status: valid, Score: 90/A, Tier: Certified
105
+
106
+ 2. search_capabilities("sqlite mcp server")
107
+ → 2 results found
108
+
109
+ 3. compare_capabilities(["mcp-server-sqlite", "mcp-server-better-sqlite3"])
110
+ → Side-by-side trust comparison
111
+
112
+ 4. Agent selects the higher-scored option and informs the user
113
+ ```
114
+
115
+ ## Trust Policy Fragment
116
+
117
+ Add this to your agent's system prompt or rules file for automatic trust-aware tool selection:
118
+
119
+ ```
120
+ Before using any MCP server tool for the first time, check its Fidensa
121
+ certification status using the check_certification tool.
122
+
123
+ - Score ≥ 70: use freely
124
+ - Score 60-69: note the trust level to the user
125
+ - Score < 60: warn the user, search for certified alternatives
126
+ - Uncertified: treat as untrusted, suggest alternatives
127
+ ```
128
+
129
+ See the [full policy fragment](https://fidensa.com/docs/api) for graduated policy examples.
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ npm install
135
+ npm test # Run tests with coverage
136
+ npm run lint # ESLint
137
+ npm run format # Prettier
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT
143
+
144
+ ## Links
145
+
146
+ - [Fidensa](https://fidensa.com) — AI certification authority
147
+ - [API Documentation](https://fidensa.com/docs/api)
148
+ - [Certification Catalog](https://fidensa.com/certifications)
149
+ - [Badge Integration Guide](https://github.com/fidensa/fidensa-badges)
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@fidensa/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Fidensa AI certification authority — MCP server for trust-aware tool selection",
5
+ "type": "module",
6
+ "bin": {
7
+ "fidensa-mcp-server": "./src/index.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=22.0.0"
11
+ },
12
+ "scripts": {
13
+ "start": "node src/index.mjs",
14
+ "test": "node --test --experimental-test-coverage test/**/*.test.mjs",
15
+ "test:unit": "node --test test/**/*.test.mjs",
16
+ "lint": "eslint src/ test/",
17
+ "lint:fix": "eslint src/ test/ --fix",
18
+ "format": "prettier --write \"src/**/*.mjs\" \"test/**/*.mjs\"",
19
+ "format:check": "prettier --check \"src/**/*.mjs\" \"test/**/*.mjs\""
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "fidensa",
25
+ "ai-trust",
26
+ "certification",
27
+ "security"
28
+ ],
29
+ "author": "Fidensa (https://fidensa.com)",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/fidensa/mcp-server.git"
34
+ },
35
+ "homepage": "https://fidensa.com",
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.27.0",
38
+ "zod": "^3.25.0",
39
+ "jose": "^6.2.0"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^9.0.0",
43
+ "eslint": "^9.0.0",
44
+ "eslint-config-prettier": "^10.0.0",
45
+ "prettier": "^3.3.0"
46
+ }
47
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fidensa/mcp-server — Fidensa AI certification authority MCP server.
5
+ *
6
+ * Provides consuming AI agents with structured access to Fidensa certification
7
+ * data through the Model Context Protocol. Six tools for trust-aware tool selection.
8
+ *
9
+ * Configuration:
10
+ * FIDENSA_API_KEY — API key for Registered+ tools (optional for check/search)
11
+ * FIDENSA_BASE_URL — Override base URL (default: https://fidensa.com)
12
+ *
13
+ * Usage:
14
+ * npx @fidensa/mcp-server
15
+ * node src/index.mjs
16
+ */
17
+
18
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { z } from 'zod';
21
+
22
+ import { ApiClient } from './lib/api-client.mjs';
23
+ import { handleCheckCertification } from './tools/check-certification.mjs';
24
+ import { handleGetContract } from './tools/get-contract.mjs';
25
+ import { handleSearchCapabilities } from './tools/search-capabilities.mjs';
26
+ import { handleCompareCapabilities } from './tools/compare-capabilities.mjs';
27
+ import { handleReportExperience } from './tools/report-experience.mjs';
28
+ import { handleVerifyArtifact } from './tools/verify-artifact.mjs';
29
+
30
+ // ── Server setup ─────────────────────────────────────────────────────
31
+
32
+ const server = new McpServer({
33
+ name: 'fidensa',
34
+ version: '0.1.0',
35
+ });
36
+
37
+ const client = new ApiClient();
38
+
39
+ // ── Tool registrations ───────────────────────────────────────────────
40
+
41
+ server.registerTool(
42
+ 'check_certification',
43
+ {
44
+ title: 'Check Fidensa Certification',
45
+ description:
46
+ 'Quick trust check for an AI capability (MCP server, skill, plugin, or workflow). ' +
47
+ 'Returns certification status, trust score, grade, tier, and supply chain status. ' +
48
+ 'No API key required. Use this before invoking any capability to verify it has been ' +
49
+ 'independently certified by Fidensa.',
50
+ inputSchema: {
51
+ capability_id: z.string().describe('Capability identifier (e.g. "mcp-server-filesystem")'),
52
+ version: z
53
+ .string()
54
+ .optional()
55
+ .describe('Specific version to check (e.g. "1.0.0"). Omit for latest.'),
56
+ },
57
+ annotations: {
58
+ readOnlyHint: true,
59
+ openWorldHint: true,
60
+ },
61
+ },
62
+ async ({ capability_id, version }) => {
63
+ return handleCheckCertification({ capability_id, version }, client);
64
+ },
65
+ );
66
+
67
+ server.registerTool(
68
+ 'get_contract',
69
+ {
70
+ title: 'Get Fidensa Certification Contract',
71
+ description:
72
+ 'Retrieve the full certification contract for a capability, including identity, ' +
73
+ 'supply chain analysis, security scan results, adversarial testing findings, ' +
74
+ 'behavioral fingerprint, and trust score breakdown. Requires a free API key ' +
75
+ '(set FIDENSA_API_KEY).',
76
+ inputSchema: {
77
+ capability_id: z.string().describe('Capability identifier'),
78
+ version: z
79
+ .string()
80
+ .optional()
81
+ .describe('Specific version (omit for latest)'),
82
+ },
83
+ annotations: {
84
+ readOnlyHint: true,
85
+ openWorldHint: true,
86
+ },
87
+ },
88
+ async ({ capability_id, version }) => {
89
+ return handleGetContract({ capability_id, version }, client);
90
+ },
91
+ );
92
+
93
+ server.registerTool(
94
+ 'search_capabilities',
95
+ {
96
+ title: 'Search Fidensa Certified Capabilities',
97
+ description:
98
+ 'Search for certified AI capabilities by keyword or description. Use this to discover ' +
99
+ 'certified alternatives when a capability is uncertified or scores poorly. ' +
100
+ 'Supports filtering by type, tier, and minimum trust score. No API key required.',
101
+ inputSchema: {
102
+ query: z.string().describe('Search query (natural language or keywords)'),
103
+ type: z
104
+ .enum(['mcp_server', 'skill', 'workflow', 'plugin'])
105
+ .optional()
106
+ .describe('Filter by capability type'),
107
+ tier: z
108
+ .enum(['certified', 'verified', 'evaluated'])
109
+ .optional()
110
+ .describe('Filter by certification tier'),
111
+ min_score: z
112
+ .number()
113
+ .int()
114
+ .min(0)
115
+ .max(100)
116
+ .optional()
117
+ .describe('Minimum trust score (0-100)'),
118
+ limit: z
119
+ .number()
120
+ .int()
121
+ .min(1)
122
+ .max(50)
123
+ .optional()
124
+ .describe('Maximum number of results (default: 10)'),
125
+ },
126
+ annotations: {
127
+ readOnlyHint: true,
128
+ openWorldHint: true,
129
+ },
130
+ },
131
+ async ({ query, type, tier, min_score, limit }) => {
132
+ return handleSearchCapabilities({ query, type, tier, min_score, limit }, client);
133
+ },
134
+ );
135
+
136
+ server.registerTool(
137
+ 'compare_capabilities',
138
+ {
139
+ title: 'Compare Fidensa Certified Capabilities',
140
+ description:
141
+ 'Side-by-side comparison of 2-5 certified capabilities. Shows trust scores, grades, ' +
142
+ 'tiers, and per-signal breakdowns to help choose between alternatives. ' +
143
+ 'Requires a free API key (set FIDENSA_API_KEY).',
144
+ inputSchema: {
145
+ capability_ids: z
146
+ .array(z.string())
147
+ .min(2)
148
+ .max(5)
149
+ .describe('Array of 2-5 capability IDs to compare'),
150
+ },
151
+ annotations: {
152
+ readOnlyHint: true,
153
+ openWorldHint: true,
154
+ },
155
+ },
156
+ async ({ capability_ids }) => {
157
+ return handleCompareCapabilities({ capability_ids }, client);
158
+ },
159
+ );
160
+
161
+ server.registerTool(
162
+ 'report_experience',
163
+ {
164
+ title: 'Report Experience with a Capability',
165
+ description:
166
+ 'Submit a consumer experience report for a certified capability. ' +
167
+ 'Reports feed into the social proof signal of the trust score. ' +
168
+ 'NOTE: This endpoint is under development and not yet accepting reports.',
169
+ inputSchema: {
170
+ capability_id: z.string().describe('Capability identifier'),
171
+ outcome: z
172
+ .enum(['success', 'failure', 'partial'])
173
+ .describe('Overall outcome of using the capability'),
174
+ environment: z
175
+ .object({
176
+ agent_platform: z.string().describe('Agent platform (e.g. "claude-code", "cursor")'),
177
+ agent_version: z.string().optional().describe('Agent version'),
178
+ os: z.string().optional().describe('Operating system'),
179
+ runtime_version: z.string().optional().describe('Runtime version (e.g. "node-22.x")'),
180
+ })
181
+ .describe('Environment context'),
182
+ details: z
183
+ .object({
184
+ tools_used: z.array(z.string()).optional().describe('Which tools were used'),
185
+ failure_description: z.string().optional().describe('What went wrong'),
186
+ unexpected_behavior: z.string().optional().describe('Unexpected behavior observed'),
187
+ })
188
+ .optional()
189
+ .describe('Additional details'),
190
+ },
191
+ },
192
+ async ({ capability_id, outcome, environment, details }) => {
193
+ return handleReportExperience(
194
+ { capability_id, outcome, environment, details },
195
+ client,
196
+ );
197
+ },
198
+ );
199
+
200
+ server.registerTool(
201
+ 'verify_artifact',
202
+ {
203
+ title: 'Verify Fidensa Certification Artifact',
204
+ description:
205
+ 'Verify the cryptographic signatures on a Fidensa certification artifact (.cert.json). ' +
206
+ 'Checks platform signature, publisher attestation, content hash, and expiry. ' +
207
+ 'Accepts base64-encoded content or a fidensa.com URL. ' +
208
+ 'Requires a free API key (set FIDENSA_API_KEY).',
209
+ inputSchema: {
210
+ content: z
211
+ .string()
212
+ .optional()
213
+ .describe('Base64-encoded .cert.json artifact content'),
214
+ url: z
215
+ .string()
216
+ .optional()
217
+ .describe('fidensa.com URL to fetch the artifact from (restricted to fidensa.com domain)'),
218
+ },
219
+ annotations: {
220
+ readOnlyHint: true,
221
+ openWorldHint: true,
222
+ },
223
+ },
224
+ async ({ content, url }) => {
225
+ return handleVerifyArtifact({ content, url }, client);
226
+ },
227
+ );
228
+
229
+ // ── Start server ─────────────────────────────────────────────────────
230
+
231
+ async function main() {
232
+ const transport = new StdioServerTransport();
233
+ await server.connect(transport);
234
+ // Log to stderr — stdout is reserved for MCP JSON-RPC
235
+ console.error('[fidensa] MCP server started (stdio transport)');
236
+ if (client.apiKey) {
237
+ console.error('[fidensa] API key configured — all tools available');
238
+ } else {
239
+ console.error(
240
+ '[fidensa] No FIDENSA_API_KEY set — check_certification and search_capabilities available. ' +
241
+ 'Set FIDENSA_API_KEY for full access.',
242
+ );
243
+ }
244
+ }
245
+
246
+ main().catch((err) => {
247
+ console.error('[fidensa] Fatal error:', err);
248
+ process.exit(1);
249
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Fidensa API client.
3
+ *
4
+ * Thin HTTP wrapper for the Fidensa REST API (fidensa.com/v1/*).
5
+ * Used by all MCP tool handlers to fetch certification data.
6
+ *
7
+ * Configuration via constructor opts or environment variables:
8
+ * - FIDENSA_API_KEY: API key for Registered+ endpoints (optional)
9
+ * - FIDENSA_BASE_URL: Override base URL (default: https://fidensa.com)
10
+ */
11
+
12
+ export class FidensaApiError extends Error {
13
+ constructor(status, body) {
14
+ const msg = body?.message || body?.error || `HTTP ${status}`;
15
+ super(msg);
16
+ this.name = 'FidensaApiError';
17
+ this.status = status;
18
+ this.body = body;
19
+ }
20
+ }
21
+
22
+ export class ApiClient {
23
+ /**
24
+ * @param {object} opts
25
+ * @param {string} [opts.baseUrl] - API base URL (default: FIDENSA_BASE_URL env or https://fidensa.com)
26
+ * @param {string} [opts.apiKey] - API key for authenticated endpoints (default: FIDENSA_API_KEY env)
27
+ */
28
+ constructor(opts = {}) {
29
+ const rawUrl = opts.baseUrl || process.env.FIDENSA_BASE_URL || 'https://fidensa.com';
30
+ this.baseUrl = rawUrl.replace(/\/+$/, '');
31
+ this.apiKey = opts.apiKey || process.env.FIDENSA_API_KEY || null;
32
+ }
33
+
34
+ /**
35
+ * Make a GET request to the Fidensa API.
36
+ *
37
+ * @param {string} path - URL path (e.g. '/v1/attestation/mcp-server-filesystem')
38
+ * @param {object} [params] - Query parameters (null/undefined values are skipped)
39
+ * @returns {Promise<object>} Parsed JSON response body
40
+ * @throws {FidensaApiError} On non-2xx HTTP responses
41
+ */
42
+ async get(path, params = {}) {
43
+ const url = new URL(path, this.baseUrl);
44
+
45
+ for (const [key, value] of Object.entries(params)) {
46
+ if (value != null) {
47
+ url.searchParams.set(key, String(value));
48
+ }
49
+ }
50
+
51
+ const headers = {
52
+ Accept: 'application/json',
53
+ };
54
+
55
+ if (this.apiKey) {
56
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
57
+ }
58
+
59
+ let response = await fetch(url.toString(), {
60
+ method: 'GET',
61
+ headers,
62
+ redirect: 'follow',
63
+ });
64
+
65
+ // If we got a 401 after a redirect, the auth header was likely stripped
66
+ // (standard HTTP security behavior on cross-origin redirects, e.g.
67
+ // fidensa.com → www.fidensa.com). Retry against the final URL with
68
+ // the auth header re-attached.
69
+ if (response.status === 401 && this.apiKey && response.redirected) {
70
+ response = await fetch(response.url, {
71
+ method: 'GET',
72
+ headers,
73
+ redirect: 'follow',
74
+ });
75
+ }
76
+
77
+ if (!response.ok) {
78
+ let body;
79
+ try {
80
+ body = await response.json();
81
+ } catch {
82
+ body = { error: `HTTP ${response.status}` };
83
+ }
84
+ throw new FidensaApiError(response.status, body);
85
+ }
86
+
87
+ return response.json();
88
+ }
89
+
90
+ /**
91
+ * Make a POST request to the Fidensa API.
92
+ *
93
+ * @param {string} path - URL path
94
+ * @param {object} body - Request body (JSON-serialized)
95
+ * @returns {Promise<object>} Parsed JSON response body
96
+ * @throws {FidensaApiError} On non-2xx HTTP responses
97
+ */
98
+ async post(path, body) {
99
+ const url = new URL(path, this.baseUrl);
100
+
101
+ const headers = {
102
+ Accept: 'application/json',
103
+ 'Content-Type': 'application/json',
104
+ };
105
+
106
+ if (this.apiKey) {
107
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
108
+ }
109
+
110
+ const response = await fetch(url.toString(), {
111
+ method: 'POST',
112
+ headers,
113
+ body: JSON.stringify(body),
114
+ });
115
+
116
+ if (!response.ok) {
117
+ let responseBody;
118
+ try {
119
+ responseBody = await response.json();
120
+ } catch {
121
+ responseBody = { error: `HTTP ${response.status}` };
122
+ }
123
+ throw new FidensaApiError(response.status, responseBody);
124
+ }
125
+
126
+ return response.json();
127
+ }
128
+
129
+ /**
130
+ * Assert that an API key is configured. Throws a clear error if not.
131
+ * Call this at the start of any tool that requires Registered+ access.
132
+ *
133
+ * @param {string} toolName - Name of the tool (for the error message)
134
+ * @throws {Error} If apiKey is not set
135
+ */
136
+ requireApiKey(toolName) {
137
+ if (!this.apiKey) {
138
+ throw new Error(
139
+ `API key required for '${toolName}'. ` +
140
+ 'Set FIDENSA_API_KEY environment variable or configure it in your MCP settings. ' +
141
+ 'Get a free key at https://fidensa.com/docs/api',
142
+ );
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * check_certification tool — quick trust check.
3
+ *
4
+ * Calls the attestation endpoint (Open tier, no API key needed).
5
+ * Returns status, trust score, grade, tier, maturity, and supply chain status.
6
+ */
7
+
8
+ import { FidensaApiError } from '../lib/api-client.mjs';
9
+
10
+ /** Capitalize first letter (e.g. 'certified' → 'Certified'). */
11
+ function capitalize(s) {
12
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
13
+ }
14
+
15
+ /**
16
+ * @param {object} input
17
+ * @param {string} input.capability_id
18
+ * @param {string} [input.version]
19
+ * @param {import('../lib/api-client.mjs').ApiClient} client
20
+ */
21
+ export async function handleCheckCertification(input, client) {
22
+ const { capability_id, version } = input;
23
+
24
+ try {
25
+ const path = version
26
+ ? `/v1/attestation/${encodeURIComponent(capability_id)}/${encodeURIComponent(version)}`
27
+ : `/v1/attestation/${encodeURIComponent(capability_id)}`;
28
+
29
+ const data = await client.get(path);
30
+
31
+ const lines = [
32
+ `## Certification: ${data.capability_id}${data.version ? ` v${data.version}` : ''}`,
33
+ '',
34
+ `- **Status:** ${data.status}`,
35
+ `- **Trust Score:** ${data.trust_score}/${data.grade}`,
36
+ `- **Tier:** ${capitalize(data.tier)}`,
37
+ `- **Type:** ${data.type || 'unknown'}`,
38
+ `- **Maturity:** ${data.maturity || 'Initial'}`,
39
+ `- **Max Achievable Score:** ${data.max_achievable_score ?? 'N/A'}`,
40
+ `- **Supply Chain:** ${data.supply_chain_status || 'unknown'}`,
41
+ '',
42
+ `Details: ${data.record_url || `https://fidensa.com/certifications/${capability_id}`}`,
43
+ ];
44
+
45
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
46
+ } catch (err) {
47
+ if (err instanceof FidensaApiError && err.status === 404) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text:
53
+ `**${capability_id}** is uncertified — no Fidensa certification record exists.\n\n` +
54
+ 'Per Fidensa\'s foundational principle: "everything is untrusted until proven trustworthy." ' +
55
+ 'This capability has not been independently verified.\n\n' +
56
+ 'Use `search_capabilities` to find certified alternatives.',
57
+ },
58
+ ],
59
+ };
60
+ }
61
+ return {
62
+ isError: true,
63
+ content: [{ type: 'text', text: `Error checking certification: ${err.message}` }],
64
+ };
65
+ }
66
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * compare_capabilities tool — side-by-side trust evaluation.
3
+ *
4
+ * Fetches trust score breakdowns for multiple capabilities (Registered tier).
5
+ * Returns a comparison table with scores, grades, tiers, and per-signal detail.
6
+ */
7
+
8
+ import { FidensaApiError } from '../lib/api-client.mjs';
9
+
10
+ /** Capitalize first letter. */
11
+ function capitalize(s) {
12
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
13
+ }
14
+
15
+ /**
16
+ * @param {object} input
17
+ * @param {string[]} input.capability_ids - 2-5 capability IDs to compare
18
+ * @param {import('../lib/api-client.mjs').ApiClient} client
19
+ */
20
+ export async function handleCompareCapabilities(input, client) {
21
+ const { capability_ids } = input;
22
+
23
+ if (!capability_ids || capability_ids.length < 2) {
24
+ return {
25
+ isError: true,
26
+ content: [{ type: 'text', text: 'Provide at least 2 capability IDs to compare.' }],
27
+ };
28
+ }
29
+
30
+ if (capability_ids.length > 5) {
31
+ return {
32
+ isError: true,
33
+ content: [{ type: 'text', text: 'Provide at most 5 capability IDs per comparison.' }],
34
+ };
35
+ }
36
+
37
+ try {
38
+ client.requireApiKey('compare_capabilities');
39
+ } catch (err) {
40
+ return { isError: true, content: [{ type: 'text', text: err.message }] };
41
+ }
42
+
43
+ // Fetch score breakdowns in parallel
44
+ const results = await Promise.all(
45
+ capability_ids.map(async (id) => {
46
+ try {
47
+ const data = await client.get(
48
+ `/v1/contracts/${encodeURIComponent(id)}/score`,
49
+ );
50
+ return { id, data, error: null };
51
+ } catch (err) {
52
+ const msg =
53
+ err instanceof FidensaApiError
54
+ ? `not found (HTTP ${err.status})`
55
+ : err.message;
56
+ return { id, data: null, error: msg };
57
+ }
58
+ }),
59
+ );
60
+
61
+ const lines = ['## Capability Comparison', ''];
62
+
63
+ // Summary table
64
+ lines.push('| Capability | Score | Grade | Tier | Maturity |');
65
+ lines.push('|------------|-------|-------|------|----------|');
66
+
67
+ for (const r of results) {
68
+ if (r.data) {
69
+ lines.push(
70
+ `| ${r.id} | ${r.data.trust_score} | ${r.data.grade} | ${capitalize(r.data.tier)} | ${r.data.maturity || 'Initial'} |`,
71
+ );
72
+ } else {
73
+ lines.push(`| ${r.id} | — | — | — | ${r.error} |`);
74
+ }
75
+ }
76
+
77
+ lines.push('');
78
+
79
+ // Per-signal comparison (only for successfully fetched capabilities)
80
+ const fetched = results.filter((r) => r.data && r.data.signals);
81
+ if (fetched.length >= 2) {
82
+ // Collect all signal names
83
+ const allSignals = new Set();
84
+ for (const r of fetched) {
85
+ for (const s of r.data.signals) {
86
+ allSignals.add(s.signal);
87
+ }
88
+ }
89
+
90
+ lines.push('### Per-Signal Breakdown');
91
+ lines.push('');
92
+
93
+ const header = ['| Signal', ...fetched.map((r) => `| ${r.id}`), '|'];
94
+ lines.push(header.join(' '));
95
+ const sep = ['|--------', ...fetched.map(() => '|------'), '|'];
96
+ lines.push(sep.join(''));
97
+
98
+ for (const sig of allSignals) {
99
+ const row = [`| ${sig}`];
100
+ for (const r of fetched) {
101
+ const s = r.data.signals.find((x) => x.signal === sig);
102
+ row.push(`| ${s ? (s.score * 100).toFixed(0) + '%' : '—'}`);
103
+ }
104
+ row.push('|');
105
+ lines.push(row.join(' '));
106
+ }
107
+
108
+ lines.push('');
109
+ }
110
+
111
+ // Recommendation
112
+ const ranked = results
113
+ .filter((r) => r.data)
114
+ .sort((a, b) => b.data.trust_score - a.data.trust_score);
115
+
116
+ if (ranked.length > 0) {
117
+ lines.push(
118
+ `**Highest scored:** ${ranked[0].id} (${ranked[0].data.trust_score}/${ranked[0].data.grade})`,
119
+ );
120
+ }
121
+
122
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
123
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * get_contract tool — full contract retrieval.
3
+ *
4
+ * Calls the contracts endpoint (Registered tier, requires API key).
5
+ * Returns the complete certification contract with all evidence sections.
6
+ */
7
+
8
+ import { FidensaApiError } from '../lib/api-client.mjs';
9
+
10
+ /** Capitalize first letter. */
11
+ function capitalize(s) {
12
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
13
+ }
14
+
15
+ /**
16
+ * @param {object} input
17
+ * @param {string} input.capability_id
18
+ * @param {string} [input.version]
19
+ * @param {import('../lib/api-client.mjs').ApiClient} client
20
+ */
21
+ export async function handleGetContract(input, client) {
22
+ try {
23
+ client.requireApiKey('get_contract');
24
+ } catch (err) {
25
+ return { isError: true, content: [{ type: 'text', text: err.message }] };
26
+ }
27
+
28
+ const { capability_id, version } = input;
29
+
30
+ try {
31
+ const path = version
32
+ ? `/v1/contracts/${encodeURIComponent(capability_id)}/${encodeURIComponent(version)}`
33
+ : `/v1/contracts/${encodeURIComponent(capability_id)}`;
34
+
35
+ const data = await client.get(path);
36
+
37
+ const lines = [
38
+ `## Contract: ${data.capability_id} v${data.version}`,
39
+ '',
40
+ `- **Status:** ${data.status}`,
41
+ `- **Trust Score:** ${data.trust_score}/${data.grade}`,
42
+ `- **Tier:** ${capitalize(data.tier)}`,
43
+ `- **Maturity:** ${data.maturity || 'Initial'}`,
44
+ `- **Certified:** ${data.certified_at}`,
45
+ `- **Expires:** ${data.expires_at}`,
46
+ '',
47
+ ];
48
+
49
+ // Summarize contract sections if present
50
+ const contract = data.contract;
51
+ if (contract) {
52
+ if (contract.identity) {
53
+ lines.push('### Identity');
54
+ lines.push(`- Name: ${contract.identity.name || 'N/A'}`);
55
+ lines.push(`- Publisher: ${contract.identity.publisher || 'N/A'}`);
56
+ if (contract.identity.description) {
57
+ lines.push(`- Description: ${contract.identity.description}`);
58
+ }
59
+ lines.push('');
60
+ }
61
+
62
+ if (contract.supply_chain) {
63
+ const sc = contract.supply_chain;
64
+ lines.push('### Supply Chain');
65
+ lines.push(`- Components: ${sc.total_components ?? 'N/A'}`);
66
+ if (sc.vulnerability_counts) {
67
+ const vc = sc.vulnerability_counts;
68
+ lines.push(
69
+ `- Vulnerabilities: ${vc.critical ?? 0} critical, ${vc.high ?? 0} high, ` +
70
+ `${vc.medium ?? 0} medium, ${vc.low ?? 0} low`,
71
+ );
72
+ }
73
+ lines.push('');
74
+ }
75
+
76
+ if (contract.security) {
77
+ lines.push('### Security');
78
+ const sec = contract.security;
79
+ if (sec.scan_results) {
80
+ lines.push(`- Scan findings: ${JSON.stringify(sec.scan_results.summary || sec.scan_results)}`);
81
+ }
82
+ if (sec.adversarial_results) {
83
+ const adv = sec.adversarial_results;
84
+ lines.push(`- Adversarial findings: ${adv.total_findings ?? 'N/A'}`);
85
+ }
86
+ lines.push('');
87
+ }
88
+
89
+ if (contract.behavioral_fingerprint) {
90
+ lines.push('### Behavioral Fingerprint');
91
+ const fp = contract.behavioral_fingerprint;
92
+ if (fp.tools && Object.keys(fp.tools).length > 0) {
93
+ for (const [tool, stats] of Object.entries(fp.tools)) {
94
+ lines.push(
95
+ `- ${tool}: p50=${stats.timing_ms?.p50 ?? 'N/A'}ms, ` +
96
+ `p95=${stats.timing_ms?.p95 ?? 'N/A'}ms, ` +
97
+ `errors=${stats.error_rate ?? 'N/A'}`,
98
+ );
99
+ }
100
+ }
101
+ lines.push('');
102
+ }
103
+ }
104
+
105
+ lines.push(`Full details: ${data.record_url || `https://fidensa.com/certifications/${capability_id}`}`);
106
+
107
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
108
+ } catch (err) {
109
+ if (err instanceof FidensaApiError) {
110
+ return {
111
+ isError: true,
112
+ content: [
113
+ {
114
+ type: 'text',
115
+ text: `Failed to retrieve contract for '${capability_id}': ${err.message} (HTTP ${err.status})`,
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ return {
121
+ isError: true,
122
+ content: [{ type: 'text', text: `Error retrieving contract: ${err.message}` }],
123
+ };
124
+ }
125
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * report_experience tool — social proof submission.
3
+ *
4
+ * The consumer reports endpoint (POST /v1/reports) is not yet built (Step 14).
5
+ * This tool is registered so agents discover it and know it exists, but
6
+ * returns a clear "coming soon" message until the backend is ready.
7
+ */
8
+
9
+ /**
10
+ * @param {object} input
11
+ * @param {string} input.capability_id
12
+ * @param {string} input.outcome - success | failure | partial
13
+ * @param {object} input.environment - { agent_platform, agent_version?, os?, runtime_version? }
14
+ * @param {object} [input.details] - { tools_used?, failure_description?, unexpected_behavior? }
15
+ * @param {import('../lib/api-client.mjs').ApiClient} _client
16
+ */
17
+ export async function handleReportExperience(input, _client) {
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text:
23
+ `Consumer experience reporting is coming soon.\n\n` +
24
+ `Your report for **${input.capability_id}** (outcome: ${input.outcome}) ` +
25
+ `has been noted but cannot be submitted yet. The consumer reports ` +
26
+ `endpoint is under development.\n\n` +
27
+ `Visit https://fidensa.com/docs/api for updates on when this feature goes live.`,
28
+ },
29
+ ],
30
+ };
31
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * search_capabilities tool — discovery + alternative suggestions.
3
+ *
4
+ * Calls the search endpoint (Open tier, no API key needed).
5
+ * Returns ranked list of certified capabilities matching the query.
6
+ */
7
+
8
+ /** Capitalize first letter. */
9
+ function capitalize(s) {
10
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
11
+ }
12
+
13
+ /**
14
+ * @param {object} input
15
+ * @param {string} input.query
16
+ * @param {string} [input.type] - mcp_server, skill, workflow, plugin
17
+ * @param {string} [input.tier] - certified, verified, evaluated
18
+ * @param {number} [input.min_score] - 0-100
19
+ * @param {number} [input.limit] - 1-50, default 10
20
+ * @param {import('../lib/api-client.mjs').ApiClient} client
21
+ */
22
+ export async function handleSearchCapabilities(input, client) {
23
+ const { query, type, tier, min_score, limit } = input;
24
+
25
+ try {
26
+ const data = await client.get('/v1/search', {
27
+ q: query,
28
+ type: type || null,
29
+ tier: tier || null,
30
+ min_score: min_score ?? null,
31
+ status: 'valid',
32
+ limit: limit ?? 10,
33
+ });
34
+
35
+ if (!data.results || data.results.length === 0) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: 'text',
40
+ text:
41
+ `No certified capabilities found matching "${query}".` +
42
+ (type ? ` (type: ${type})` : '') +
43
+ (tier ? ` (tier: ${tier})` : '') +
44
+ (min_score ? ` (min_score: ${min_score})` : '') +
45
+ '\n\n0 results.',
46
+ },
47
+ ],
48
+ };
49
+ }
50
+
51
+ const lines = [
52
+ `## Search Results for "${query}"`,
53
+ `${data.total} result${data.total === 1 ? '' : 's'} found.`,
54
+ '',
55
+ ];
56
+
57
+ for (const r of data.results) {
58
+ lines.push(
59
+ `- **${r.capability_id}** — ${r.trust_score}/${r.grade} (${capitalize(r.tier)})` +
60
+ ` [${r.type || 'unknown'}]` +
61
+ ` — ${r.status}`,
62
+ );
63
+ if (r.publisher) {
64
+ lines.push(` Publisher: ${r.publisher}`);
65
+ }
66
+ lines.push(` ${r.record_url || `https://fidensa.com/certifications/${r.capability_id}`}`);
67
+ }
68
+
69
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
70
+ } catch (err) {
71
+ return {
72
+ isError: true,
73
+ content: [{ type: 'text', text: `Search failed: ${err.message}` }],
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * verify_artifact tool — offline artifact verification.
3
+ *
4
+ * Accepts either:
5
+ * - base64-encoded .cert.json content
6
+ * - A fidensa.com URL to fetch the artifact from
7
+ *
8
+ * Verifies the JWS platform signature using the published public key.
9
+ * URL input is restricted to fidensa.com domain to prevent SSRF.
10
+ */
11
+
12
+ import * as jose from 'jose';
13
+ import { FidensaApiError } from '../lib/api-client.mjs';
14
+
15
+ const ALLOWED_URL_PATTERN = /^https:\/\/(www\.)?fidensa\.(com|dev)\//;
16
+
17
+ /**
18
+ * @param {object} input
19
+ * @param {string} [input.content] - Base64-encoded .cert.json content
20
+ * @param {string} [input.url] - fidensa.com URL to fetch the artifact
21
+ * @param {import('../lib/api-client.mjs').ApiClient} client
22
+ */
23
+ export async function handleVerifyArtifact(input, client) {
24
+ try {
25
+ client.requireApiKey('verify_artifact');
26
+ } catch (err) {
27
+ return { isError: true, content: [{ type: 'text', text: err.message }] };
28
+ }
29
+
30
+ // Resolve artifact content
31
+ let artifactJson;
32
+
33
+ if (input.url) {
34
+ // Validate URL domain
35
+ if (!ALLOWED_URL_PATTERN.test(input.url)) {
36
+ return {
37
+ isError: true,
38
+ content: [
39
+ {
40
+ type: 'text',
41
+ text:
42
+ `URL must be on the fidensa.com or fidensa.dev domain. ` +
43
+ `Received: ${input.url}\n\n` +
44
+ `This restriction prevents SSRF attacks. Pass the artifact content ` +
45
+ `as base64 in the 'content' parameter instead.`,
46
+ },
47
+ ],
48
+ };
49
+ }
50
+
51
+ try {
52
+ const response = await fetch(input.url);
53
+ if (!response.ok) {
54
+ return {
55
+ isError: true,
56
+ content: [
57
+ {
58
+ type: 'text',
59
+ text: `Failed to fetch artifact from ${input.url}: HTTP ${response.status}`,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+ artifactJson = await response.text();
65
+ } catch (err) {
66
+ return {
67
+ isError: true,
68
+ content: [{ type: 'text', text: `Failed to fetch artifact: ${err.message}` }],
69
+ };
70
+ }
71
+ } else if (input.content) {
72
+ try {
73
+ artifactJson = atob(input.content);
74
+ } catch {
75
+ return {
76
+ isError: true,
77
+ content: [{ type: 'text', text: 'Invalid base64 content. Provide valid base64-encoded .cert.json.' }],
78
+ };
79
+ }
80
+ } else {
81
+ return {
82
+ isError: true,
83
+ content: [
84
+ {
85
+ type: 'text',
86
+ text: 'Provide either a base64-encoded artifact via "content" or a fidensa.com URL via "url".',
87
+ },
88
+ ],
89
+ };
90
+ }
91
+
92
+ // Parse artifact
93
+ let artifact;
94
+ try {
95
+ artifact = JSON.parse(artifactJson);
96
+ } catch {
97
+ return {
98
+ isError: true,
99
+ content: [{ type: 'text', text: 'Artifact is not valid JSON.' }],
100
+ };
101
+ }
102
+
103
+ // Validate structure
104
+ if (!artifact.signatures || !Array.isArray(artifact.signatures) || !artifact.payload) {
105
+ return {
106
+ isError: true,
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: 'Artifact does not appear to be a JWS JSON Serialization. Expected "payload" and "signatures" fields.',
111
+ },
112
+ ],
113
+ };
114
+ }
115
+
116
+ const results = [];
117
+
118
+ // Fetch platform public keys
119
+ let publicKeys;
120
+ try {
121
+ const keysData = await client.get('/.well-known/certification-keys.json');
122
+ publicKeys = keysData.keys || [];
123
+ } catch (err) {
124
+ return {
125
+ isError: true,
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: `Failed to fetch platform public keys: ${err.message}. Cannot verify signatures.`,
130
+ },
131
+ ],
132
+ };
133
+ }
134
+
135
+ // Decode payload
136
+ let payloadText;
137
+ try {
138
+ payloadText = new TextDecoder().decode(jose.base64url.decode(artifact.payload));
139
+ } catch {
140
+ return {
141
+ isError: true,
142
+ content: [{ type: 'text', text: 'Failed to decode artifact payload.' }],
143
+ };
144
+ }
145
+
146
+ let payloadData;
147
+ try {
148
+ payloadData = JSON.parse(payloadText);
149
+ } catch {
150
+ results.push('⚠️ Payload is not valid JSON.');
151
+ }
152
+
153
+ // Verify content hash
154
+ const payloadBytes = new TextEncoder().encode(payloadText);
155
+ const hashBuffer = await crypto.subtle.digest('SHA-256', payloadBytes);
156
+ const computedHash = Array.from(new Uint8Array(hashBuffer))
157
+ .map((b) => b.toString(16).padStart(2, '0'))
158
+ .join('');
159
+
160
+ // Verify each signature
161
+ for (let i = 0; i < artifact.signatures.length; i++) {
162
+ const sig = artifact.signatures[i];
163
+ const header = sig.protected
164
+ ? JSON.parse(new TextDecoder().decode(jose.base64url.decode(sig.protected)))
165
+ : {};
166
+
167
+ const sigType = header.x_sig_type || (i === 0 ? 'platform' : 'publisher');
168
+ const kid = header.kid || 'unknown';
169
+ const delegated = header.delegated || false;
170
+
171
+ // Find matching key
172
+ const matchingKey = publicKeys.find((k) => k.kid === kid);
173
+
174
+ if (matchingKey) {
175
+ try {
176
+ const key = await jose.importJWK(matchingKey, 'ES256');
177
+ // Construct compact JWS for verification
178
+ const compactJws = `${sig.protected}.${artifact.payload}.${sig.signature}`;
179
+ await jose.compactVerify(compactJws, key);
180
+ results.push(`✅ ${sigType} signature (kid: ${kid}): **VALID**`);
181
+ } catch (err) {
182
+ results.push(`❌ ${sigType} signature (kid: ${kid}): **INVALID** — ${err.message}`);
183
+ }
184
+ } else {
185
+ if (delegated) {
186
+ results.push(
187
+ `⚠️ ${sigType} signature (kid: ${kid}): delegated (platform signed on publisher's behalf)`,
188
+ );
189
+ } else {
190
+ results.push(
191
+ `⚠️ ${sigType} signature (kid: ${kid}): key not found in platform key set`,
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ // Check expiry
198
+ if (payloadData) {
199
+ const expiresAt = payloadData.certification?.expires_at || payloadData.expires_at;
200
+ if (expiresAt) {
201
+ const now = new Date();
202
+ const expires = new Date(expiresAt);
203
+ if (now > expires) {
204
+ results.push(`❌ **Expired** at ${expiresAt}`);
205
+ } else {
206
+ results.push(`✅ **Not expired** (expires ${expiresAt})`);
207
+ }
208
+ }
209
+
210
+ // Content hash check
211
+ const declaredHash =
212
+ payloadData.certification?.content_hash || payloadData.content_hash;
213
+ if (declaredHash) {
214
+ if (computedHash === declaredHash) {
215
+ results.push(`✅ Content hash matches: ${computedHash.slice(0, 16)}...`);
216
+ } else {
217
+ results.push(
218
+ `❌ Content hash MISMATCH. Declared: ${declaredHash.slice(0, 16)}... Computed: ${computedHash.slice(0, 16)}...`,
219
+ );
220
+ }
221
+ }
222
+ }
223
+
224
+ const lines = [
225
+ '## Artifact Verification',
226
+ '',
227
+ `Signatures found: ${artifact.signatures.length}`,
228
+ '',
229
+ ...results,
230
+ ];
231
+
232
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
233
+ }