@ship-with-ai/stackmap-mcp 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/index.mjs +136 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShipWithAI (Alessandro Magionami & Manuel Salvatore Martone)
|
|
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,44 @@
|
|
|
1
|
+
# @ship-with-ai/stackmap-mcp
|
|
2
|
+
|
|
3
|
+
Query **[StackMap](https://stackmap.shipwithai.xyz)** — the curated map of the open-source AI stack — from any MCP client. 200+ repos, every connection typed (`complements` / `alternative` / `built_with`) and human-reviewed, with the *why* written down — including **when NOT to use a tool**.
|
|
4
|
+
|
|
5
|
+
No auth. No state. No telemetry. Reads the site's static JSON API; the catalog is date-versioned (`v2026.07.24` style) so you always know how fresh the map is.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
**Claude Code**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
claude mcp add stackmap -- npx -y @ship-with-ai/stackmap-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Cursor** — `.cursor/mcp.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{ "mcpServers": { "stackmap": { "command": "npx", "args": ["-y", "stackmap-mcp"] } } }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Codex CLI** — `~/.codex/config.toml`:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[mcp_servers.stackmap]
|
|
25
|
+
command = "npx"
|
|
26
|
+
args = ["-y", "stackmap-mcp"]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
| Tool | What it answers |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `search_catalog` | "What's on the map for *local voice transcription*?" |
|
|
34
|
+
| `get_repo` | "Tell me about langgraph — including when NOT to use it." |
|
|
35
|
+
| `get_alternatives` | "What replaces crewai, and when do I pick which?" |
|
|
36
|
+
| `continue_stack` | "I use langgraph + chroma — what pairs well next?" |
|
|
37
|
+
|
|
38
|
+
Every answer carries the curator's written why — the sentence a colleague would tell you over your shoulder, not a link dump.
|
|
39
|
+
|
|
40
|
+
## How the data is made
|
|
41
|
+
|
|
42
|
+
Nothing on the map auto-publishes: an LLM proposes, a human approves, sharpens, or refuses — and both the [refusal ledger and the dismissal ledger are public](https://stackmap.shipwithai.xyz/how-we-curate). Suggest a repo at [/suggest](https://stackmap.shipwithai.xyz/suggest); dispute any edge from its repo page.
|
|
43
|
+
|
|
44
|
+
MIT · by [ShipWithAI](https://stackmap.shipwithai.xyz/about)
|
package/index.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// stackmap-mcp — query the StackMap catalog (stackmap.shipwithai.xyz) from any MCP client.
|
|
3
|
+
// Reads the site's static JSON API; no auth, no state, no telemetry. The catalog is
|
|
4
|
+
// CalVer-stamped (meta.version = date of last content change) and every relationship is
|
|
5
|
+
// typed + human-reviewed with a written "why".
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
const SITE = process.env.STACKMAP_SITE_URL?.replace(/\/+$/, '') || 'https://stackmap.shipwithai.xyz'
|
|
11
|
+
const TTL_MS = 15 * 60 * 1000
|
|
12
|
+
|
|
13
|
+
let _cache = null // { at, catalog }
|
|
14
|
+
async function catalog() {
|
|
15
|
+
if (_cache && Date.now() - _cache.at < TTL_MS) return _cache.catalog
|
|
16
|
+
const res = await fetch(`${SITE}/api/catalog.json`)
|
|
17
|
+
if (!res.ok) throw new Error(`catalog fetch failed: HTTP ${res.status}`)
|
|
18
|
+
const c = await res.json()
|
|
19
|
+
_cache = { at: Date.now(), catalog: c }
|
|
20
|
+
return c
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const stamp = c => `StackMap catalog v${c.version || '?'} · ${Object.keys(c.repos).length} repos · every connection human-reviewed`
|
|
24
|
+
const text = s => ({ content: [{ type: 'text', text: s }] })
|
|
25
|
+
const fail = s => ({ content: [{ type: 'text', text: s }], isError: true })
|
|
26
|
+
|
|
27
|
+
// Compact, citable card for one repo — the shape every tool answers with.
|
|
28
|
+
function card(c, slug, { withEdges = true } = {}) {
|
|
29
|
+
const r = c.repos[slug]
|
|
30
|
+
if (!r) return null
|
|
31
|
+
const head = `## ${r.name} (${r.owner}/${slug}) — ★${(r.stars || 0).toLocaleString('en-US')}${r.language ? ` · ${r.language}` : ''}${(r.topics || []).length ? ` · ${r.topics.join(', ')}` : ''}`
|
|
32
|
+
const lines = [head, '', r.summary || '', '', `**Curator's take:** ${r.curator_note || '(none)'}`]
|
|
33
|
+
if (withEdges) {
|
|
34
|
+
const rel = r.related || {}
|
|
35
|
+
for (const [type, label] of [['complements', 'Pairs well with'], ['alternative', 'Alternatives'], ['built_with', 'Built with / foundation for']]) {
|
|
36
|
+
const items = rel[type] || []
|
|
37
|
+
if (!items.length) continue
|
|
38
|
+
lines.push('', `**${label}:**`)
|
|
39
|
+
for (const e of items) lines.push(`- ${e.name} (${e.slug}) — ${e.why}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lines.push('', `Page: ${r.url || `${SITE}/repos/${r.owner}/${slug}`}`)
|
|
43
|
+
return lines.join('\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function score(r, terms) {
|
|
47
|
+
const hay = [r.name, r.slug, r.summary, r.curator_note, (r.topics || []).join(' ')].join(' ').toLowerCase()
|
|
48
|
+
let s = 0
|
|
49
|
+
for (const t of terms) {
|
|
50
|
+
if (!hay.includes(t)) return 0 // AND semantics — every term must appear
|
|
51
|
+
s += (r.name?.toLowerCase().includes(t) ? 3 : 0) + (r.summary?.toLowerCase().includes(t) ? 2 : 1)
|
|
52
|
+
}
|
|
53
|
+
return s + Math.log10((r.stars || 0) + 10) // tie-break by popularity, gently
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const server = new McpServer({ name: 'stackmap', version: '0.1.0' })
|
|
57
|
+
|
|
58
|
+
server.registerTool('search_catalog', {
|
|
59
|
+
title: 'Search the StackMap catalog',
|
|
60
|
+
description: 'Search 200+ curated open-source AI/agent repos by keywords. Matches name, summary, curator note and topics. Returns ranked matches with the human-written "when to use it (and when not)".',
|
|
61
|
+
inputSchema: { query: z.string().describe('keywords, e.g. "local voice transcription" or "agent memory"'), limit: z.number().int().min(1).max(25).optional().describe('max results (default 8)') },
|
|
62
|
+
}, async ({ query, limit = 8 }) => {
|
|
63
|
+
const c = await catalog()
|
|
64
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
|
|
65
|
+
if (!terms.length) return fail('empty query')
|
|
66
|
+
const hits = Object.entries(c.repos)
|
|
67
|
+
.map(([slug, r]) => ({ slug, r, s: score(r, terms) }))
|
|
68
|
+
.filter(h => h.s > 0)
|
|
69
|
+
.sort((a, b) => b.s - a.s)
|
|
70
|
+
.slice(0, limit)
|
|
71
|
+
if (!hits.length) return text(`No matches for "${query}". ${stamp(c)}\nTry broader terms, or browse topics: ${Object.keys(c.topics).join(', ')}.`)
|
|
72
|
+
const body = hits.map(h => `- **${h.r.name}** (${h.slug}) ★${(h.r.stars || 0).toLocaleString('en-US')} — ${h.r.summary}`).join('\n')
|
|
73
|
+
return text(`${stamp(c)}\n\n${body}\n\nUse get_repo(slug) for the curator's take and typed relationships.`)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
server.registerTool('get_repo', {
|
|
77
|
+
title: 'Get one repo, fully mapped',
|
|
78
|
+
description: 'Full StackMap entry for a repo: summary, the opinionated curator note (including when NOT to use it), and every typed, human-reviewed relationship with its written why.',
|
|
79
|
+
inputSchema: { slug: z.string().describe('repo slug, e.g. "langgraph" (try search_catalog if unsure)') },
|
|
80
|
+
}, async ({ slug }) => {
|
|
81
|
+
const c = await catalog()
|
|
82
|
+
const k = slug.toLowerCase()
|
|
83
|
+
const out = card(c, k)
|
|
84
|
+
if (!out) return fail(`Unknown slug "${slug}". Use search_catalog to find it.`)
|
|
85
|
+
return text(`${stamp(c)}\n\n${out}`)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
server.registerTool('get_alternatives', {
|
|
89
|
+
title: 'Alternatives to a repo',
|
|
90
|
+
description: 'Human-reviewed alternatives for a repo — same job, different tool — each with the written "when to pick which".',
|
|
91
|
+
inputSchema: { slug: z.string().describe('repo slug') },
|
|
92
|
+
}, async ({ slug }) => {
|
|
93
|
+
const c = await catalog()
|
|
94
|
+
const r = c.repos[slug.toLowerCase()]
|
|
95
|
+
if (!r) return fail(`Unknown slug "${slug}". Use search_catalog to find it.`)
|
|
96
|
+
const alts = r.related?.alternative || []
|
|
97
|
+
if (!alts.length) return text(`${stamp(c)}\n\nNo curated alternatives recorded for ${r.name} yet — an honest gap, not an oversight. Compare pages: ${SITE}/alternatives/${slug}`)
|
|
98
|
+
const body = alts.map(e => `- **${e.name}** (${e.slug}) — ${e.why}`).join('\n')
|
|
99
|
+
return text(`${stamp(c)}\n\n**Alternatives to ${r.name}:**\n${body}`)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
server.registerTool('continue_stack', {
|
|
103
|
+
title: 'Continue your stack',
|
|
104
|
+
description: 'Given repos you already use, suggests what pairs well next — curated complements ranked by curator confidence, each with the written why.',
|
|
105
|
+
inputSchema: { slugs: z.array(z.string()).min(1).describe('repo slugs already in your stack, e.g. ["langgraph","chroma"]') },
|
|
106
|
+
}, async ({ slugs }) => {
|
|
107
|
+
const c = await catalog()
|
|
108
|
+
const have = new Set(slugs.map(s => s.toLowerCase()))
|
|
109
|
+
const unknown = [...have].filter(s => !c.repos[s])
|
|
110
|
+
const candidates = new Map() // slug -> { conf, whys: [from → why] }
|
|
111
|
+
for (const s of have) {
|
|
112
|
+
const r = c.repos[s]
|
|
113
|
+
if (!r) continue
|
|
114
|
+
for (const e of r.related?.complements || []) {
|
|
115
|
+
if (have.has(e.slug)) continue
|
|
116
|
+
const cur = candidates.get(e.slug) || { conf: 0, whys: [] }
|
|
117
|
+
cur.conf = Math.max(cur.conf, e.confidence || 0)
|
|
118
|
+
cur.whys.push(`with ${r.name}: ${e.why}`)
|
|
119
|
+
candidates.set(e.slug, cur)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const ranked = [...candidates.entries()].sort((a, b) => b[1].conf - a[1].conf).slice(0, 8)
|
|
123
|
+
const parts = [stamp(c)]
|
|
124
|
+
if (unknown.length) parts.push(`(unknown slugs ignored: ${unknown.join(', ')})`)
|
|
125
|
+
if (!ranked.length) return text(`${parts.join('\n')}\n\nNo curated complements found beyond what you have. Browse the graph: ${SITE}/`)
|
|
126
|
+
parts.push('', '**What pairs well next:**')
|
|
127
|
+
for (const [slug, { whys }] of ranked) {
|
|
128
|
+
const r = c.repos[slug]
|
|
129
|
+
parts.push(`- **${r.name}** (${slug}) ★${(r.stars || 0).toLocaleString('en-US')}`)
|
|
130
|
+
for (const w of whys) parts.push(` - ${w}`)
|
|
131
|
+
}
|
|
132
|
+
return text(parts.join('\n'))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const transport = new StdioServerTransport()
|
|
136
|
+
await server.connect(transport)
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ship-with-ai/stackmap-mcp",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "MCP server for StackMap \u2014 the curated map of the open-source AI stack. Search 200+ human-reviewed repos, typed relationships (complements / alternative / built_with), and the written why behind every connection.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"stackmap-mcp": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"modelcontextprotocol",
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"stackmap",
|
|
22
|
+
"curated",
|
|
23
|
+
"knowledge-graph"
|
|
24
|
+
],
|
|
25
|
+
"author": "ShipWithAI",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://stackmap.shipwithai.xyz",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
30
|
+
"zod": "^3.24.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Ship-With-AI/stackmap-mcp.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Ship-With-AI/stackmap-mcp/issues"
|
|
38
|
+
}
|
|
39
|
+
}
|