@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/index.mjs +136 -0
  4. 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
+ }