@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 +149 -0
- package/package.json +47 -0
- package/src/index.mjs +249 -0
- package/src/lib/api-client.mjs +145 -0
- package/src/tools/check-certification.mjs +66 -0
- package/src/tools/compare-capabilities.mjs +123 -0
- package/src/tools/get-contract.mjs +125 -0
- package/src/tools/report-experience.mjs +31 -0
- package/src/tools/search-capabilities.mjs +76 -0
- package/src/tools/verify-artifact.mjs +233 -0
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
|
+
}
|