@lpm-registry/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 +687 -0
- package/bin/mcp-server.js +11 -0
- package/lib/api.js +145 -0
- package/lib/auth.js +42 -0
- package/lib/cache.js +73 -0
- package/lib/cli.js +99 -0
- package/lib/constants.js +35 -0
- package/lib/format.js +67 -0
- package/lib/resolve-version.js +49 -0
- package/lib/server.js +357 -0
- package/lib/tools/add.js +79 -0
- package/lib/tools/api-docs.js +78 -0
- package/lib/tools/browse-source.js +111 -0
- package/lib/tools/get-install-command.js +73 -0
- package/lib/tools/install.js +51 -0
- package/lib/tools/llm-context.js +78 -0
- package/lib/tools/package-context.js +168 -0
- package/lib/tools/package-info.js +156 -0
- package/lib/tools/package-skills.js +100 -0
- package/lib/tools/packages-by-owner.js +66 -0
- package/lib/tools/pool-stats.js +38 -0
- package/lib/tools/quality-report.js +50 -0
- package/lib/tools/search-owners.js +58 -0
- package/lib/tools/search.js +232 -0
- package/lib/tools/user-info.js +38 -0
- package/package.json +52 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { registryGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL } from "../constants.js"
|
|
3
|
+
import { errorResponse, parseName, textResponse } from "../format.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch the quality report for an LPM package.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ name: string }} params
|
|
9
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
10
|
+
*/
|
|
11
|
+
export async function qualityReport({ name }, ctx) {
|
|
12
|
+
let queryName
|
|
13
|
+
try {
|
|
14
|
+
const parsed = parseName(name)
|
|
15
|
+
queryName = `${parsed.owner}.${parsed.name}`
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return errorResponse(err.message)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const token = await ctx.getToken()
|
|
21
|
+
const cacheKey = `quality:${queryName}:auth=${!!token}`
|
|
22
|
+
|
|
23
|
+
const cached = ctx.cache.get(cacheKey)
|
|
24
|
+
if (cached) return cached
|
|
25
|
+
|
|
26
|
+
const baseUrl = ctx.getBaseUrl()
|
|
27
|
+
const result = await registryGet(
|
|
28
|
+
`/quality?name=${encodeURIComponent(queryName)}`,
|
|
29
|
+
token,
|
|
30
|
+
baseUrl,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
if (result.status === 404) {
|
|
35
|
+
return errorResponse(`Package ${queryName} not found.`)
|
|
36
|
+
}
|
|
37
|
+
if (result.status === 403) {
|
|
38
|
+
return errorResponse(
|
|
39
|
+
"Access denied. Private packages require authentication.",
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return errorResponse(
|
|
43
|
+
result.data?.error || `Request failed (${result.status})`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response = textResponse(JSON.stringify(result.data, null, 2))
|
|
48
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
49
|
+
return response
|
|
50
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { searchGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL } from "../constants.js"
|
|
3
|
+
import { errorResponse, textResponse } from "../format.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Search for users or organizations on the LPM registry.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ query: string, limit?: number }} params
|
|
9
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
10
|
+
*/
|
|
11
|
+
export async function searchOwners({ query, limit }, ctx) {
|
|
12
|
+
if (!query || query.trim().length < 1) {
|
|
13
|
+
return errorResponse("Search query is required.")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const q = query.trim()
|
|
17
|
+
const safeLimit = Math.min(Math.max(limit || 5, 1), 10)
|
|
18
|
+
|
|
19
|
+
const cacheKey = `search-owners:${q}:${safeLimit}`
|
|
20
|
+
const cached = ctx.cache.get(cacheKey)
|
|
21
|
+
if (cached) return cached
|
|
22
|
+
|
|
23
|
+
const token = await ctx.getToken()
|
|
24
|
+
const baseUrl = ctx.getBaseUrl()
|
|
25
|
+
const params = new URLSearchParams({ q, limit: String(safeLimit) })
|
|
26
|
+
|
|
27
|
+
const result = await searchGet(`/owners?${params.toString()}`, token, baseUrl)
|
|
28
|
+
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
return errorResponse(
|
|
31
|
+
result.data?.error || `Search failed (${result.status})`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const owners = result.data?.owners || []
|
|
36
|
+
|
|
37
|
+
if (owners.length === 0) {
|
|
38
|
+
const response = textResponse(`No users or organizations found for "${q}".`)
|
|
39
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
40
|
+
return response
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const lines = [
|
|
44
|
+
`Found ${owners.length} profile${owners.length === 1 ? "" : "s"}:\n`,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
for (const owner of owners) {
|
|
48
|
+
const type = owner.type === "org" ? "org" : "user"
|
|
49
|
+
const name =
|
|
50
|
+
owner.name && owner.name !== owner.slug ? ` (${owner.name})` : ""
|
|
51
|
+
const bio = owner.bio ? ` — ${owner.bio.substring(0, 80)}` : ""
|
|
52
|
+
lines.push(`- @${owner.slug}${name} [${type}]${bio}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = textResponse(lines.join("\n"))
|
|
56
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
57
|
+
return response
|
|
58
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { searchGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL, ERROR_MESSAGES } from "../constants.js"
|
|
3
|
+
import { errorResponse, textResponse } from "../format.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unified LPM package search.
|
|
7
|
+
*
|
|
8
|
+
* Uses dual-endpoint strategy:
|
|
9
|
+
* - Semantic path (/packages?mode=semantic): when only query + ecosystem/limit are provided
|
|
10
|
+
* - Explore path (/packages/explore): when any structured filter is active
|
|
11
|
+
*
|
|
12
|
+
* @param {object} params
|
|
13
|
+
* @param {string} [params.query] - Search text (required unless category provided)
|
|
14
|
+
* @param {string} [params.category] - Category slug filter
|
|
15
|
+
* @param {string} [params.ecosystem] - Ecosystem filter (js, swift, xcframework)
|
|
16
|
+
* @param {string} [params.distribution] - Distribution mode filter (marketplace, pool, private)
|
|
17
|
+
* @param {string} [params.packageType] - Package type filter
|
|
18
|
+
* @param {string} [params.sort] - Sort order (newest, popular, name)
|
|
19
|
+
* @param {boolean} [params.hasTypes] - TypeScript types filter
|
|
20
|
+
* @param {string} [params.moduleType] - Module type filter (esm, cjs, dual)
|
|
21
|
+
* @param {string} [params.license] - License filter
|
|
22
|
+
* @param {string} [params.minNodeVersion] - Min Node version filter
|
|
23
|
+
* @param {number} [params.limit] - Max results (1-50, default 10)
|
|
24
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
25
|
+
*/
|
|
26
|
+
export async function search(params, ctx) {
|
|
27
|
+
const {
|
|
28
|
+
query,
|
|
29
|
+
category,
|
|
30
|
+
distribution,
|
|
31
|
+
packageType,
|
|
32
|
+
sort,
|
|
33
|
+
hasTypes,
|
|
34
|
+
moduleType,
|
|
35
|
+
license,
|
|
36
|
+
minNodeVersion,
|
|
37
|
+
} = params
|
|
38
|
+
|
|
39
|
+
// Validation: at least query or category required
|
|
40
|
+
if (!query?.trim() && !category) {
|
|
41
|
+
return errorResponse(ERROR_MESSAGES.searchNoParams)
|
|
42
|
+
}
|
|
43
|
+
if (query && query.trim().length < 2) {
|
|
44
|
+
return errorResponse("Search query must be at least 2 characters.")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const token = await ctx.getToken()
|
|
48
|
+
const baseUrl = ctx.getBaseUrl()
|
|
49
|
+
|
|
50
|
+
// Determine which path to use
|
|
51
|
+
const hasStructuredFilters =
|
|
52
|
+
category ||
|
|
53
|
+
distribution ||
|
|
54
|
+
packageType ||
|
|
55
|
+
sort ||
|
|
56
|
+
hasTypes ||
|
|
57
|
+
moduleType ||
|
|
58
|
+
license ||
|
|
59
|
+
minNodeVersion
|
|
60
|
+
|
|
61
|
+
// Build deterministic cache key from all params
|
|
62
|
+
const cacheKey = buildCacheKey(params)
|
|
63
|
+
const cached = ctx.cache.get(cacheKey)
|
|
64
|
+
if (cached) return cached
|
|
65
|
+
|
|
66
|
+
let response
|
|
67
|
+
if (hasStructuredFilters) {
|
|
68
|
+
response = await exploreSearch(params, token, baseUrl)
|
|
69
|
+
} else {
|
|
70
|
+
response = await semanticSearch(params, token, baseUrl)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (response.isError) return response
|
|
74
|
+
|
|
75
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
76
|
+
return response
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Semantic search path — best quality for natural language queries.
|
|
81
|
+
* Uses /api/search/packages?mode=semantic
|
|
82
|
+
*/
|
|
83
|
+
async function semanticSearch({ query, ecosystem, limit }, token, baseUrl) {
|
|
84
|
+
const q = query.trim()
|
|
85
|
+
const safeLimit = Math.min(Math.max(limit || 10, 1), 20)
|
|
86
|
+
|
|
87
|
+
const searchParams = new URLSearchParams({
|
|
88
|
+
q,
|
|
89
|
+
mode: "semantic",
|
|
90
|
+
limit: String(safeLimit),
|
|
91
|
+
})
|
|
92
|
+
if (ecosystem) searchParams.set("ecosystem", ecosystem)
|
|
93
|
+
|
|
94
|
+
const result = await searchGet(
|
|
95
|
+
`/packages?${searchParams.toString()}`,
|
|
96
|
+
token,
|
|
97
|
+
baseUrl,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
return errorResponse(
|
|
102
|
+
result.data?.error || `Search failed (${result.status})`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const packages = result.data?.packages || []
|
|
107
|
+
return formatResults(packages, query)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Explore search path — supports all structured filters.
|
|
112
|
+
* Uses /api/search/packages/explore
|
|
113
|
+
*/
|
|
114
|
+
async function exploreSearch(params, token, baseUrl) {
|
|
115
|
+
const {
|
|
116
|
+
query,
|
|
117
|
+
category,
|
|
118
|
+
ecosystem,
|
|
119
|
+
distribution,
|
|
120
|
+
packageType,
|
|
121
|
+
sort,
|
|
122
|
+
hasTypes,
|
|
123
|
+
moduleType,
|
|
124
|
+
license,
|
|
125
|
+
minNodeVersion,
|
|
126
|
+
limit,
|
|
127
|
+
} = params
|
|
128
|
+
|
|
129
|
+
const safeLimit = Math.min(Math.max(limit || 10, 1), 50)
|
|
130
|
+
|
|
131
|
+
const searchParams = new URLSearchParams()
|
|
132
|
+
if (query?.trim()) searchParams.set("q", query.trim())
|
|
133
|
+
if (category) searchParams.set("category", category)
|
|
134
|
+
if (ecosystem) searchParams.set("ecosystem", ecosystem)
|
|
135
|
+
if (distribution) searchParams.set("distribution", distribution)
|
|
136
|
+
if (packageType) searchParams.set("packageType", packageType)
|
|
137
|
+
if (sort) searchParams.set("sort", sort)
|
|
138
|
+
if (hasTypes) searchParams.set("hasTypes", "true")
|
|
139
|
+
if (moduleType) searchParams.set("moduleType", moduleType)
|
|
140
|
+
if (license) searchParams.set("license", license)
|
|
141
|
+
if (minNodeVersion) searchParams.set("minNodeVersion", minNodeVersion)
|
|
142
|
+
searchParams.set("limit", String(safeLimit))
|
|
143
|
+
|
|
144
|
+
const result = await searchGet(
|
|
145
|
+
`/packages/explore?${searchParams.toString()}`,
|
|
146
|
+
token,
|
|
147
|
+
baseUrl,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
return errorResponse(
|
|
152
|
+
result.data?.error || `Search failed (${result.status})`,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const packages = result.data?.packages || []
|
|
157
|
+
return formatResults(packages, query)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Format search results as readable text for LLM consumption.
|
|
162
|
+
* Handles both semantic and explore response shapes.
|
|
163
|
+
*/
|
|
164
|
+
function formatResults(packages, query) {
|
|
165
|
+
if (packages.length === 0) {
|
|
166
|
+
const label = query?.trim() ? ` for "${query.trim()}"` : ""
|
|
167
|
+
return textResponse(`No packages found${label}.`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const lines = [
|
|
171
|
+
`Found ${packages.length} package${packages.length === 1 ? "" : "s"}:\n`,
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
for (const pkg of packages) {
|
|
175
|
+
const owner = pkg.ownerSlug || pkg.owner
|
|
176
|
+
const name = `${owner}.${pkg.name}`
|
|
177
|
+
|
|
178
|
+
// Type badge (skip default "package")
|
|
179
|
+
const typeLabel =
|
|
180
|
+
pkg.packageType && pkg.packageType !== "package"
|
|
181
|
+
? ` [${pkg.packageType}]`
|
|
182
|
+
: ""
|
|
183
|
+
|
|
184
|
+
// Ecosystem badge (skip default "js")
|
|
185
|
+
const ecosystemLabel =
|
|
186
|
+
pkg.ecosystem && pkg.ecosystem !== "js" ? ` {${pkg.ecosystem}}` : ""
|
|
187
|
+
|
|
188
|
+
// Distribution mode
|
|
189
|
+
const distLabel = pkg.distributionMode ? ` (${pkg.distributionMode})` : ""
|
|
190
|
+
|
|
191
|
+
const desc = pkg.description ? ` — ${pkg.description}` : ""
|
|
192
|
+
const downloads = pkg.downloadCount
|
|
193
|
+
? ` (${Number(pkg.downloadCount).toLocaleString()} downloads)`
|
|
194
|
+
: ""
|
|
195
|
+
|
|
196
|
+
lines.push(
|
|
197
|
+
`- ${name}${typeLabel}${ecosystemLabel}${distLabel}${desc}${downloads}`,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Second line with extra metadata (from explore path)
|
|
201
|
+
const meta = []
|
|
202
|
+
if (pkg.qualityScore != null) meta.push(`Quality: ${pkg.qualityScore}`)
|
|
203
|
+
if (pkg.category) meta.push(`Category: ${pkg.category}`)
|
|
204
|
+
if (pkg.tags?.length > 0) meta.push(`Tags: ${pkg.tags.join(", ")}`)
|
|
205
|
+
if (meta.length > 0) {
|
|
206
|
+
lines.push(` ${meta.join(" | ")}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return textResponse(lines.join("\n"))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build a deterministic cache key from all search params.
|
|
215
|
+
*/
|
|
216
|
+
function buildCacheKey(params) {
|
|
217
|
+
const parts = [
|
|
218
|
+
"search",
|
|
219
|
+
params.query?.trim() || "",
|
|
220
|
+
params.category || "",
|
|
221
|
+
params.ecosystem || "",
|
|
222
|
+
params.distribution || "",
|
|
223
|
+
params.packageType || "",
|
|
224
|
+
params.sort || "",
|
|
225
|
+
params.hasTypes ? "types" : "",
|
|
226
|
+
params.moduleType || "",
|
|
227
|
+
params.license || "",
|
|
228
|
+
params.minNodeVersion || "",
|
|
229
|
+
String(params.limit || 10),
|
|
230
|
+
]
|
|
231
|
+
return parts.join(":")
|
|
232
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { registryGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL, ERROR_MESSAGES } from "../constants.js"
|
|
3
|
+
import { errorResponse, textResponse } from "../format.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch info about the authenticated LPM user.
|
|
7
|
+
* Requires authentication.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} _params
|
|
10
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
11
|
+
*/
|
|
12
|
+
export async function userInfo(_params, ctx) {
|
|
13
|
+
const token = await ctx.getToken()
|
|
14
|
+
if (!token) {
|
|
15
|
+
return errorResponse(ERROR_MESSAGES.noToken)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cacheKey = "user-info"
|
|
19
|
+
|
|
20
|
+
const cached = ctx.cache.get(cacheKey)
|
|
21
|
+
if (cached) return cached
|
|
22
|
+
|
|
23
|
+
const baseUrl = ctx.getBaseUrl()
|
|
24
|
+
const result = await registryGet("/-/whoami", token, baseUrl)
|
|
25
|
+
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
if (result.status === 401) {
|
|
28
|
+
return errorResponse(ERROR_MESSAGES.unauthorized)
|
|
29
|
+
}
|
|
30
|
+
return errorResponse(
|
|
31
|
+
result.data?.error || `Request failed (${result.status})`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const response = textResponse(JSON.stringify(result.data, null, 2))
|
|
36
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
37
|
+
return response
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lpm-registry/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for the LPM package registry — exposes package info, quality reports, name checks, pool stats, and marketplace search as AI tools",
|
|
5
|
+
"author": "LPM <hello@lpm.dev> (https://lpm.dev)",
|
|
6
|
+
"homepage": "https://lpm.dev",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"lpm",
|
|
10
|
+
"mcp",
|
|
11
|
+
"model-context-protocol",
|
|
12
|
+
"package-registry",
|
|
13
|
+
"ai-tools"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "lib/server.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"lpm-mcp-server": "./bin/mcp-server.js"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./lib/server.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"lib",
|
|
26
|
+
"!lib/__tests__",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"prepare": "husky",
|
|
31
|
+
"start": "node bin/mcp-server.js",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"lint": "biome check .",
|
|
35
|
+
"lint:fix": "biome check --write ."
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
39
|
+
"zod": "^3.24.0"
|
|
40
|
+
},
|
|
41
|
+
"optionalDependencies": {
|
|
42
|
+
"keytar": "^7.9.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "^2.3.10",
|
|
46
|
+
"husky": "^9.1.7",
|
|
47
|
+
"vitest": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|