mcp-server-mcpindex 0.2.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/package.json +47 -0
- package/src/index.mjs +286 -0
- package/src/trust.mjs +164 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mcp-server-mcpindex` are recorded here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-05-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`check_tool_trust(server_id, tool_name)` MCP tool.** Pre-invocation advisory trust verdict for a specific tool on an MCP server. Returns the v1 verdict contract (directive, status, dimensions, freshness, honest_limits). Fail-CLOSED: returns `UNVERIFIED` + `status: ERROR` when the upstream endpoint is unreachable, returns 404, times out, or returns malformed data. Never coerces to ALLOW.
|
|
12
|
+
- **`assess_server(server_id)` MCP tool.** Aggregated pre-flight verdict across all tools on a server. Same verdict shape as `check_tool_trust`. Use for "is THIS server worth integrating?" decisions.
|
|
13
|
+
- **`src/trust.mjs` library export.** The trust client is exported as a plain ES module (`checkToolTrust`, `assessServer`, `VERDICT_CONTRACT_VERSION`, `V1_HONEST_LIMITS`) so non-MCP consumers can call it directly.
|
|
14
|
+
- **Verdict contract version pin.** `verdict_contract_version: "1.0.0"` is included on every verdict response.
|
|
15
|
+
- **v1 honest limits floor.** Every verdict ships with at least `conformance_monitored_not_enforced`, `calibrated_false_v1`, and `advisory_deployment` in `honest_limits`. Upstream cannot remove the floor.
|
|
16
|
+
- **README integration guide.** Worked example of wrapping `check_tool_trust` as a pre-invocation gate (LangChain / DSPy / Mastra / Composio / raw LLM-tool-call style), including the fail-CLOSED handling for UNVERIFIED.
|
|
17
|
+
- **Test suite.** `node --test test/trust.test.mjs`. Covers the fail-CLOSED invariant (unreachable, 404, timeout, malformed payload, missing args), the happy-path verdict shape, dimension normalization, unknown-directive downgrade, and the honest_limits floor.
|
|
18
|
+
- **npm scripts.** `npm run build` (syntax check) and `npm test`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Package description updated to mention the advisory trust verdicts.
|
|
23
|
+
- Embedded server version bumped to `0.2.0`.
|
|
24
|
+
|
|
25
|
+
### Notes for integrators
|
|
26
|
+
|
|
27
|
+
`check_tool_trust` is **v1 advisory**. Conformance is monitored, not enforced. The verdict is a recommendation; the agent (or the human reviewing the agent) is the decision-maker. Never treat UNVERIFIED as ALLOW.
|
|
28
|
+
|
|
29
|
+
## [0.1.0] - 2026-04-30
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Initial release. Tools: `recommend_mcp_for_task`, `search_mcp_servers`, `get_install_command`, `compare_servers`.
|
|
34
|
+
- Stdio MCP transport via `@modelcontextprotocol/sdk`.
|
|
35
|
+
- Backend defaults to `https://mcpindex.ai`; override with `MCPINDEX_API_BASE`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bhartis LLC
|
|
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,198 @@
|
|
|
1
|
+
# mcp-server-mcpindex
|
|
2
|
+
|
|
3
|
+
> An MCP server for finding MCP servers, plus advisory trust verdicts agent frameworks can call before invoking a tool.
|
|
4
|
+
|
|
5
|
+
A drop-in MCP server that lets your agent discover, compare, install, and pre-flight other MCP servers from inside the agent loop. Backed by [mcpindex.ai](https://mcpindex.ai) - the agent-native index of 3,500+ MCP servers indexed daily from the official registry.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g mcp-server-mcpindex
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use it from Claude Desktop
|
|
14
|
+
|
|
15
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"mcpindex": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "mcp-server-mcpindex"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Restart Claude Desktop. Then ask:
|
|
29
|
+
|
|
30
|
+
> "Find me an MCP server that can read PDFs and write the contents to S3."
|
|
31
|
+
|
|
32
|
+
Claude calls `recommend_mcp_for_task` and returns the top 3 ranked servers with install commands.
|
|
33
|
+
|
|
34
|
+
## Use it from Cursor
|
|
35
|
+
|
|
36
|
+
Add to `.cursor/mcp.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"mcpindex": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "mcp-server-mcpindex"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Use it from Cline
|
|
50
|
+
|
|
51
|
+
Add to your Cline settings:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx -y mcp-server-mcpindex
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tools exposed
|
|
58
|
+
|
|
59
|
+
| Tool | What it does |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `recommend_mcp_for_task` | Pass a natural-language task. Returns top 3 servers with reasoning, install commands, quality scores. |
|
|
62
|
+
| `search_mcp_servers` | Keyword + semantic search across the full registry. Optional category filter. |
|
|
63
|
+
| `get_install_command` | Get the exact install JSON for a server + a target client (Claude Desktop, Cursor, Cline, Zed). |
|
|
64
|
+
| `compare_servers` | Side-by-side comparison of 2-5 servers - quality scores, install paths, env vars. |
|
|
65
|
+
| `check_tool_trust` | Pre-invocation advisory verdict for a specific tool on a server. Fail-CLOSED: returns UNVERIFIED when no verdict on file. |
|
|
66
|
+
| `assess_server` | Aggregated pre-flight verdict across all tools on a server. Same shape as `check_tool_trust`. |
|
|
67
|
+
|
|
68
|
+
## Agent-framework integration: pre-invocation trust gate
|
|
69
|
+
|
|
70
|
+
`check_tool_trust` is the integration surface that lets agent frameworks (Composio, Mastra, LangChain, DSPy, raw LLM-tool-call loops) ask "is this tool safe to invoke right now?" before dispatching the call.
|
|
71
|
+
|
|
72
|
+
### Verdict contract (v1)
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
{
|
|
76
|
+
"directive": "ALLOW" | "DENY" | "REVIEW" | "UNVERIFIED",
|
|
77
|
+
"status": "EVALUATED" | "STALE" | "ERROR",
|
|
78
|
+
"dimensions": [
|
|
79
|
+
{ "id": "tool_safety", "verdict": "PASS", "severity": "INFO" }
|
|
80
|
+
],
|
|
81
|
+
"expires_at": "2026-06-30T00:00:00Z",
|
|
82
|
+
"honest_limits": [
|
|
83
|
+
"conformance_monitored_not_enforced",
|
|
84
|
+
"calibrated_false_v1",
|
|
85
|
+
"advisory_deployment"
|
|
86
|
+
],
|
|
87
|
+
"verdict_contract_version": "1.0.0",
|
|
88
|
+
"server_id": "github",
|
|
89
|
+
"tool_name": "create_pull_request",
|
|
90
|
+
"source_url": "https://mcpindex.ai/api/v1/trust/tool/github/create_pull_request",
|
|
91
|
+
"fetched_at": "2026-05-28T18:42:11.118Z"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The free-tier verdict ships directives + dimensions + freshness. Evidence quotes, LLM rationale, and chain history are paid-tier surfaces and intentionally omitted here.
|
|
96
|
+
|
|
97
|
+
### Honest limits (pin these to your gate UI)
|
|
98
|
+
|
|
99
|
+
Every v1 verdict ships with these three caveats, and your gate SHOULD surface them on every dispatch decision:
|
|
100
|
+
|
|
101
|
+
1. `conformance_monitored_not_enforced` - publishers self-declare; mcpindex monitors drift but does not block at the network layer.
|
|
102
|
+
2. `calibrated_false_v1` - dimension severities are not yet calibrated against real-world incident data.
|
|
103
|
+
3. `advisory_deployment` - the verdict is advisory; the agent (or human reviewing the agent) is the decision-maker.
|
|
104
|
+
|
|
105
|
+
History anchoring: OTS Bitcoin-anchored history; Bitcoin-finalized at N=6 confirmations (~1 hr); pending in ~10 min. Sub-window precision asserted, not proven.
|
|
106
|
+
|
|
107
|
+
### Integration pattern (LangChain-style, direct LLM-tool-call convention)
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
111
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
112
|
+
|
|
113
|
+
const mcpindex = new Client({ name: 'gate', version: '1.0.0' }, { capabilities: {} });
|
|
114
|
+
await mcpindex.connect(new StdioClientTransport({
|
|
115
|
+
command: 'npx', args: ['-y', 'mcp-server-mcpindex'],
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// gateToolCall wraps any agent tool dispatch. Plug it in front of
|
|
119
|
+
// the LangChain / DSPy / Mastra / Composio tool-call hook.
|
|
120
|
+
async function gateToolCall({ serverId, toolName, invoke, askHuman }) {
|
|
121
|
+
const res = await mcpindex.callTool({
|
|
122
|
+
name: 'check_tool_trust',
|
|
123
|
+
arguments: { server_id: serverId, tool_name: toolName },
|
|
124
|
+
});
|
|
125
|
+
const verdict = JSON.parse(res.content[0].text);
|
|
126
|
+
|
|
127
|
+
// Pin the v1 caveats in the audit log no matter what.
|
|
128
|
+
audit.log({ verdict, caveats: verdict.honest_limits });
|
|
129
|
+
|
|
130
|
+
switch (verdict.directive) {
|
|
131
|
+
case 'ALLOW':
|
|
132
|
+
return invoke();
|
|
133
|
+
|
|
134
|
+
case 'DENY':
|
|
135
|
+
throw new Error(
|
|
136
|
+
`mcpindex denied ${serverId}/${toolName}: ${JSON.stringify(verdict.dimensions)}`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
case 'REVIEW':
|
|
140
|
+
// Fail-CLOSED to human. Do NOT auto-execute on REVIEW.
|
|
141
|
+
return askHuman({ verdict, action: `${serverId}/${toolName}` });
|
|
142
|
+
|
|
143
|
+
case 'UNVERIFIED':
|
|
144
|
+
// No verdict on file (or upstream unreachable). Fail-CLOSED.
|
|
145
|
+
// Recommend human review. Do NOT fail-open to invoke().
|
|
146
|
+
return askHuman({
|
|
147
|
+
verdict,
|
|
148
|
+
action: `${serverId}/${toolName}`,
|
|
149
|
+
note: 'No trust verdict on file. Human review required before first use.',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
// Unknown directive. Fail-CLOSED.
|
|
154
|
+
return askHuman({ verdict, action: `${serverId}/${toolName}` });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### The load-bearing rule: never fail-open
|
|
160
|
+
|
|
161
|
+
If the verdict endpoint is unreachable, returns 404, times out, or returns malformed JSON, `check_tool_trust` returns `directive: "UNVERIFIED"` + `status: "ERROR"`. It never silently coerces to ALLOW. Your gate code SHOULD treat UNVERIFIED as "human review required", never as "looks fine, ship it."
|
|
162
|
+
|
|
163
|
+
This is tested. See `test/trust.test.mjs`.
|
|
164
|
+
|
|
165
|
+
### Using the library directly (without MCP)
|
|
166
|
+
|
|
167
|
+
The trust client is also exported as a plain ES module:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { checkToolTrust, assessServer } from 'mcp-server-mcpindex/src/trust.mjs';
|
|
171
|
+
|
|
172
|
+
const verdict = await checkToolTrust({
|
|
173
|
+
serverId: 'github',
|
|
174
|
+
toolName: 'create_pull_request',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (verdict.directive !== 'ALLOW') {
|
|
178
|
+
// Hand to a human, log, or block.
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Backend
|
|
183
|
+
|
|
184
|
+
By default, calls go to `https://mcpindex.ai`. Override with `MCPINDEX_API_BASE=...` if you self-host.
|
|
185
|
+
|
|
186
|
+
The free tier is rate-limited to 60 req/min/IP. Paid keys are coming for higher throughput and the full evidence-bearing verdict (evidence quotes, LLM rationale, chain history).
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT.
|
|
191
|
+
|
|
192
|
+
## Project
|
|
193
|
+
|
|
194
|
+
- Website: [mcpindex.ai](https://mcpindex.ai)
|
|
195
|
+
- Methodology: [mcpindex.ai/methodology](https://mcpindex.ai/methodology)
|
|
196
|
+
- Source: [github.com/mcpindex-ai](https://github.com/mcpindex-ai)
|
|
197
|
+
|
|
198
|
+
Unofficial. Not affiliated with Anthropic.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-server-mcpindex",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "An MCP server for finding MCP servers, plus advisory trust verdicts (check_tool_trust, assess_server) for agent frameworks. Drop-in for Claude Desktop, Cursor, Cline, Zed.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"claude",
|
|
9
|
+
"cursor",
|
|
10
|
+
"agent",
|
|
11
|
+
"discovery",
|
|
12
|
+
"trust",
|
|
13
|
+
"tool-safety",
|
|
14
|
+
"governance"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://mcpindex.ai",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/mcpindex-ai/mcp-server-mcpindex"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/mcpindex-ai/mcp-server-mcpindex/issues"
|
|
23
|
+
},
|
|
24
|
+
"author": "Bhartis LLC <hello@mcpindex.ai>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "src/index.mjs",
|
|
28
|
+
"bin": {
|
|
29
|
+
"mcp-server-mcpindex": "src/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "node --check src/index.mjs && node --check src/trust.mjs",
|
|
42
|
+
"test": "node --test test/*.test.mjs"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mcp-server-mcpindex - an MCP server for finding MCP servers.
|
|
3
|
+
// Backend: api.mcpindex.ai (versioned, free tier - no key needed for v0).
|
|
4
|
+
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
export { checkToolTrust, assessServer, VERDICT_CONTRACT_VERSION, V1_HONEST_LIMITS } from './trust.mjs';
|
|
12
|
+
|
|
13
|
+
const API_BASE = process.env.MCPINDEX_API_BASE ?? 'https://mcpindex.ai';
|
|
14
|
+
const PKG_VERSION = '0.2.0';
|
|
15
|
+
|
|
16
|
+
const server = new Server(
|
|
17
|
+
{ name: 'mcp-server-mcpindex', version: PKG_VERSION },
|
|
18
|
+
{ capabilities: { tools: {} } },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const TOOLS = [
|
|
22
|
+
{
|
|
23
|
+
name: 'recommend_mcp_for_task',
|
|
24
|
+
description:
|
|
25
|
+
'Recommend the best MCP servers for a natural-language task. Returns top 3 ranked picks with reasoning, install commands, and quality scores. Use this when the user asks for the right MCP server for a task they want to do.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
task: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description:
|
|
32
|
+
'Natural-language description of the task, e.g. "read PDFs and write to S3" or "search GitHub and open a PR".',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['task'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'search_mcp_servers',
|
|
40
|
+
description:
|
|
41
|
+
'Keyword + semantic search across the full MCP server registry. Use when the user knows what tool category they want but not which server.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
query: { type: 'string', description: 'Search query.' },
|
|
46
|
+
category: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description:
|
|
49
|
+
'Optional category filter (e.g. database, browser, github, productivity).',
|
|
50
|
+
},
|
|
51
|
+
limit: {
|
|
52
|
+
type: 'number',
|
|
53
|
+
description: 'Max results (default 10, max 50).',
|
|
54
|
+
default: 10,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ['query'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'get_install_command',
|
|
62
|
+
description:
|
|
63
|
+
'Get the exact install command for a given MCP server and client. Returns a JSON block ready to paste into the client config.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
server_slug: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description:
|
|
70
|
+
'Slug of the server (from search_mcp_servers or recommend_mcp_for_task results).',
|
|
71
|
+
},
|
|
72
|
+
client: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
enum: ['claude-desktop', 'cursor', 'cline', 'zed'],
|
|
75
|
+
description: 'Target client.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ['server_slug', 'client'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'compare_servers',
|
|
83
|
+
description:
|
|
84
|
+
'Side-by-side comparison of 2-5 MCP servers - quality scores, install paths, transport types, env vars.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
slugs: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
items: { type: 'string' },
|
|
91
|
+
minItems: 2,
|
|
92
|
+
maxItems: 5,
|
|
93
|
+
description: 'Server slugs to compare.',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['slugs'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'check_tool_trust',
|
|
101
|
+
description:
|
|
102
|
+
'Pre-invocation trust check for a specific tool on an MCP server. Returns an advisory verdict object (directive ALLOW | DENY | REVIEW | UNVERIFIED, dimensions, freshness). v1 advisory; conformance monitored not enforced; verdicts may be UNVERIFIED if not yet probed. Agents SHOULD treat UNVERIFIED as "human review required", never as ALLOW.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
server_id: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Server slug (e.g. "github", "filesystem"). Same id used by search_mcp_servers.',
|
|
109
|
+
},
|
|
110
|
+
tool_name: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'Tool name as exposed by the server (e.g. "create_pull_request").',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
required: ['server_id', 'tool_name'],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'assess_server',
|
|
120
|
+
description:
|
|
121
|
+
'Aggregated pre-flight trust assessment across all tools on an MCP server. Same verdict shape as check_tool_trust. Use for "is THIS server worth integrating?" decisions. v1 advisory; conformance monitored not enforced; verdicts may be UNVERIFIED if not yet probed.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
server_id: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'Server slug to assess.',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ['server_id'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
136
|
+
|
|
137
|
+
async function api(path) {
|
|
138
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
139
|
+
headers: { 'User-Agent': 'mcp-server-mcpindex/0.1.0' },
|
|
140
|
+
});
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
throw new Error(`mcpindex API ${res.status}: ${await res.text()}`);
|
|
143
|
+
}
|
|
144
|
+
return res.json();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
148
|
+
const { name, arguments: args } = req.params;
|
|
149
|
+
try {
|
|
150
|
+
let result;
|
|
151
|
+
switch (name) {
|
|
152
|
+
case 'recommend_mcp_for_task': {
|
|
153
|
+
const data = await api(
|
|
154
|
+
`/api/v1/recommend?task=${encodeURIComponent(args.task)}`,
|
|
155
|
+
);
|
|
156
|
+
result = formatRecommend(data);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'search_mcp_servers': {
|
|
160
|
+
const params = new URLSearchParams({
|
|
161
|
+
q: args.query,
|
|
162
|
+
limit: String(Math.min(50, args.limit ?? 10)),
|
|
163
|
+
});
|
|
164
|
+
if (args.category) params.set('category', args.category);
|
|
165
|
+
const data = await api(`/api/v1/search?${params.toString()}`);
|
|
166
|
+
result = formatSearch(data);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'get_install_command': {
|
|
170
|
+
const data = await api(`/api/v1/server/${encodeURIComponent(args.server_slug)}`);
|
|
171
|
+
result = formatInstall(data, args.client);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'compare_servers': {
|
|
175
|
+
const rows = await Promise.all(
|
|
176
|
+
args.slugs.map((s) => api(`/api/v1/server/${encodeURIComponent(s)}`)),
|
|
177
|
+
);
|
|
178
|
+
result = formatCompare(rows);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'check_tool_trust': {
|
|
182
|
+
const { checkToolTrust } = await import('./trust.mjs');
|
|
183
|
+
const verdict = await checkToolTrust({
|
|
184
|
+
serverId: args.server_id,
|
|
185
|
+
toolName: args.tool_name,
|
|
186
|
+
apiBase: API_BASE,
|
|
187
|
+
userAgent: `mcp-server-mcpindex/${PKG_VERSION}`,
|
|
188
|
+
});
|
|
189
|
+
result = JSON.stringify(verdict, null, 2);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'assess_server': {
|
|
193
|
+
const { assessServer } = await import('./trust.mjs');
|
|
194
|
+
const verdict = await assessServer({
|
|
195
|
+
serverId: args.server_id,
|
|
196
|
+
apiBase: API_BASE,
|
|
197
|
+
userAgent: `mcp-server-mcpindex/${PKG_VERSION}`,
|
|
198
|
+
});
|
|
199
|
+
result = JSON.stringify(verdict, null, 2);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
204
|
+
}
|
|
205
|
+
return { content: [{ type: 'text', text: result }] };
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return {
|
|
208
|
+
isError: true,
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: 'text',
|
|
212
|
+
text: `Error calling ${name}: ${err instanceof Error ? err.message : err}`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
function formatRecommend(data) {
|
|
220
|
+
const lines = [`Top ${data.recommendations.length} for: "${data.task}"`, ''];
|
|
221
|
+
for (const r of data.recommendations) {
|
|
222
|
+
lines.push(`[${r.rank}] ${r.title} · QS ${r.qualityScore}/100 · ${r.category}`);
|
|
223
|
+
lines.push(` ${r.name}@${r.qualityScore ? '' : ''}`);
|
|
224
|
+
lines.push(` ${r.reasoning}`);
|
|
225
|
+
const install = r.installs.npm
|
|
226
|
+
? `npx -y ${r.installs.npm}`
|
|
227
|
+
: r.installs.pypi
|
|
228
|
+
? `uvx ${r.installs.pypi}`
|
|
229
|
+
: r.installs.docker
|
|
230
|
+
? `docker run --rm -i ${r.installs.docker}`
|
|
231
|
+
: r.installs.remote
|
|
232
|
+
? `Remote: ${r.installs.remote}`
|
|
233
|
+
: 'manual install - see detail page';
|
|
234
|
+
lines.push(` $ ${install}`);
|
|
235
|
+
lines.push(` ${r.url}`);
|
|
236
|
+
lines.push('');
|
|
237
|
+
}
|
|
238
|
+
lines.push(`Source: ${data.note ?? ''}`);
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatSearch(data) {
|
|
243
|
+
const lines = [`${data.total} results for: "${data.query}"`, ''];
|
|
244
|
+
for (const r of data.results) {
|
|
245
|
+
lines.push(`- ${r.title} (${r.name}) · QS ${r.qualityScore}/100 · ${r.category}`);
|
|
246
|
+
lines.push(` ${r.description}`);
|
|
247
|
+
lines.push(` ${r.url}`);
|
|
248
|
+
}
|
|
249
|
+
return lines.join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatInstall(server, client) {
|
|
253
|
+
// server here is the per-server JSON we expose at /api/v1/server/<slug>.
|
|
254
|
+
// Returns a code block with the appropriate install snippet.
|
|
255
|
+
const target = (server.installs ?? []).find((i) => i.client === client) ?? server.installs?.[0];
|
|
256
|
+
if (!target) return `No install path available for ${server.name}`;
|
|
257
|
+
return [
|
|
258
|
+
`${server.title} (${server.name}) - ${client}`,
|
|
259
|
+
'',
|
|
260
|
+
target.json
|
|
261
|
+
? '```json\n' + target.json + '\n```'
|
|
262
|
+
: '```bash\n' + target.command + '\n```',
|
|
263
|
+
target.notes ? '\n' + target.notes : '',
|
|
264
|
+
`\n${server.url ?? `https://mcpindex.ai/server/${server.slug}`}`,
|
|
265
|
+
].join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatCompare(rows) {
|
|
269
|
+
const header = ['name', 'category', 'quality', 'install', 'env vars'];
|
|
270
|
+
const data = rows.map((r) => [
|
|
271
|
+
r.name,
|
|
272
|
+
r.category,
|
|
273
|
+
String(r.qualityScore ?? ''),
|
|
274
|
+
r.installs?.[0]?.label ?? 'manual',
|
|
275
|
+
String(r.envVars?.length ?? 0),
|
|
276
|
+
]);
|
|
277
|
+
const widths = header.map((h, i) =>
|
|
278
|
+
Math.max(h.length, ...data.map((row) => row[i].length)),
|
|
279
|
+
);
|
|
280
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ');
|
|
281
|
+
return [fmt(header), fmt(widths.map((w) => '-'.repeat(w))), ...data.map(fmt)].join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const transport = new StdioServerTransport();
|
|
285
|
+
await server.connect(transport);
|
|
286
|
+
console.error('[mcp-server-mcpindex] connected via stdio');
|
package/src/trust.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// trust.mjs - check_tool_trust / assess_server verdict client.
|
|
2
|
+
//
|
|
3
|
+
// Contract (v1 advisory, free tier):
|
|
4
|
+
// {
|
|
5
|
+
// directive: "ALLOW" | "DENY" | "REVIEW" | "UNVERIFIED",
|
|
6
|
+
// status: "EVALUATED" | "STALE" | "ERROR",
|
|
7
|
+
// dimensions: [{ id, verdict, severity }],
|
|
8
|
+
// expires_at: string | null,
|
|
9
|
+
// honest_limits: string[],
|
|
10
|
+
// verdict_contract_version: "1.0.0",
|
|
11
|
+
// server_id, tool_name?, source_url, fetched_at
|
|
12
|
+
// }
|
|
13
|
+
//
|
|
14
|
+
// Fail-CLOSED: if the upstream endpoint is unreachable, returns 404, or
|
|
15
|
+
// returns malformed data, we return directive=UNVERIFIED, status=ERROR.
|
|
16
|
+
// We NEVER fake an ALLOW. This is the load-bearing safety invariant.
|
|
17
|
+
|
|
18
|
+
export const VERDICT_CONTRACT_VERSION = '1.0.0';
|
|
19
|
+
|
|
20
|
+
export const V1_HONEST_LIMITS = Object.freeze([
|
|
21
|
+
'conformance_monitored_not_enforced',
|
|
22
|
+
'calibrated_false_v1',
|
|
23
|
+
'advisory_deployment',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const VALID_DIRECTIVES = new Set(['ALLOW', 'DENY', 'REVIEW', 'UNVERIFIED']);
|
|
27
|
+
const VALID_STATUSES = new Set(['EVALUATED', 'STALE', 'ERROR']);
|
|
28
|
+
const VALID_VERDICTS = new Set(['PASS', 'FAIL', 'UNVERIFIED']);
|
|
29
|
+
const VALID_SEVERITIES = new Set(['INFO', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']);
|
|
30
|
+
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
32
|
+
|
|
33
|
+
function unverifiedResponse({ serverId, toolName, sourceUrl, reason }) {
|
|
34
|
+
return {
|
|
35
|
+
directive: 'UNVERIFIED',
|
|
36
|
+
status: 'ERROR',
|
|
37
|
+
dimensions: [],
|
|
38
|
+
expires_at: null,
|
|
39
|
+
honest_limits: [...V1_HONEST_LIMITS, `unverified_reason:${reason}`],
|
|
40
|
+
verdict_contract_version: VERDICT_CONTRACT_VERSION,
|
|
41
|
+
server_id: serverId,
|
|
42
|
+
...(toolName ? { tool_name: toolName } : {}),
|
|
43
|
+
source_url: sourceUrl,
|
|
44
|
+
fetched_at: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeDimensions(raw) {
|
|
49
|
+
if (!Array.isArray(raw)) return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const d of raw) {
|
|
52
|
+
if (!d || typeof d !== 'object') continue;
|
|
53
|
+
const id = typeof d.id === 'string' ? d.id : null;
|
|
54
|
+
const verdict = VALID_VERDICTS.has(d.verdict) ? d.verdict : 'UNVERIFIED';
|
|
55
|
+
const severity = VALID_SEVERITIES.has(d.severity) ? d.severity : 'INFO';
|
|
56
|
+
if (!id) continue;
|
|
57
|
+
out.push({ id, verdict, severity });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shapeVerdict({ raw, serverId, toolName, sourceUrl }) {
|
|
63
|
+
// Defensive: if upstream returns garbage, treat as UNVERIFIED.
|
|
64
|
+
if (!raw || typeof raw !== 'object') {
|
|
65
|
+
return unverifiedResponse({ serverId, toolName, sourceUrl, reason: 'empty_payload' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const directive = VALID_DIRECTIVES.has(raw.directive) ? raw.directive : 'UNVERIFIED';
|
|
69
|
+
const status = VALID_STATUSES.has(raw.status)
|
|
70
|
+
? raw.status
|
|
71
|
+
: directive === 'UNVERIFIED'
|
|
72
|
+
? 'ERROR'
|
|
73
|
+
: 'EVALUATED';
|
|
74
|
+
const dimensions = normalizeDimensions(raw.dimensions);
|
|
75
|
+
const expires_at = typeof raw.expires_at === 'string' ? raw.expires_at : null;
|
|
76
|
+
|
|
77
|
+
// honest_limits: always include the v1 defaults; merge in any extras the
|
|
78
|
+
// server sent. Never let the upstream remove the v1 floor.
|
|
79
|
+
const extra = Array.isArray(raw.honest_limits)
|
|
80
|
+
? raw.honest_limits.filter((s) => typeof s === 'string')
|
|
81
|
+
: [];
|
|
82
|
+
const honest_limits = Array.from(new Set([...V1_HONEST_LIMITS, ...extra]));
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
directive,
|
|
86
|
+
status,
|
|
87
|
+
dimensions,
|
|
88
|
+
expires_at,
|
|
89
|
+
honest_limits,
|
|
90
|
+
verdict_contract_version: VERDICT_CONTRACT_VERSION,
|
|
91
|
+
server_id: serverId,
|
|
92
|
+
...(toolName ? { tool_name: toolName } : {}),
|
|
93
|
+
source_url: sourceUrl,
|
|
94
|
+
fetched_at: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fetchVerdict({ url, userAgent, fetchImpl, timeoutMs }) {
|
|
99
|
+
const f = fetchImpl ?? fetch;
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const t = setTimeout(() => controller.abort(), timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
102
|
+
try {
|
|
103
|
+
const res = await f(url, {
|
|
104
|
+
headers: { 'User-Agent': userAgent, Accept: 'application/json' },
|
|
105
|
+
signal: controller.signal,
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
return { ok: false, reason: `http_${res.status}` };
|
|
109
|
+
}
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
return { ok: true, data };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const reason = err && err.name === 'AbortError' ? 'timeout' : 'network_error';
|
|
114
|
+
return { ok: false, reason };
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(t);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function checkToolTrust({
|
|
121
|
+
serverId,
|
|
122
|
+
toolName,
|
|
123
|
+
apiBase = 'https://mcpindex.ai',
|
|
124
|
+
userAgent = `mcp-server-mcpindex/${VERDICT_CONTRACT_VERSION}`,
|
|
125
|
+
fetchImpl,
|
|
126
|
+
timeoutMs,
|
|
127
|
+
}) {
|
|
128
|
+
if (!serverId || !toolName) {
|
|
129
|
+
return unverifiedResponse({
|
|
130
|
+
serverId: serverId ?? '',
|
|
131
|
+
toolName: toolName ?? '',
|
|
132
|
+
sourceUrl: '',
|
|
133
|
+
reason: 'missing_args',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const url = `${apiBase}/api/v1/trust/tool/${encodeURIComponent(serverId)}/${encodeURIComponent(toolName)}`;
|
|
137
|
+
const r = await fetchVerdict({ url, userAgent, fetchImpl, timeoutMs });
|
|
138
|
+
if (!r.ok) {
|
|
139
|
+
return unverifiedResponse({ serverId, toolName, sourceUrl: url, reason: r.reason });
|
|
140
|
+
}
|
|
141
|
+
return shapeVerdict({ raw: r.data, serverId, toolName, sourceUrl: url });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function assessServer({
|
|
145
|
+
serverId,
|
|
146
|
+
apiBase = 'https://mcpindex.ai',
|
|
147
|
+
userAgent = `mcp-server-mcpindex/${VERDICT_CONTRACT_VERSION}`,
|
|
148
|
+
fetchImpl,
|
|
149
|
+
timeoutMs,
|
|
150
|
+
}) {
|
|
151
|
+
if (!serverId) {
|
|
152
|
+
return unverifiedResponse({
|
|
153
|
+
serverId: '',
|
|
154
|
+
sourceUrl: '',
|
|
155
|
+
reason: 'missing_args',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const url = `${apiBase}/api/v1/trust/server/${encodeURIComponent(serverId)}`;
|
|
159
|
+
const r = await fetchVerdict({ url, userAgent, fetchImpl, timeoutMs });
|
|
160
|
+
if (!r.ok) {
|
|
161
|
+
return unverifiedResponse({ serverId, sourceUrl: url, reason: r.reason });
|
|
162
|
+
}
|
|
163
|
+
return shapeVerdict({ raw: r.data, serverId, sourceUrl: url });
|
|
164
|
+
}
|