@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
package/lib/server.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { getBaseUrl, getToken } from "./auth.js"
|
|
4
|
+
import { MemoryCache } from "./cache.js"
|
|
5
|
+
import { add } from "./tools/add.js"
|
|
6
|
+
import { apiDocs } from "./tools/api-docs.js"
|
|
7
|
+
import { browseSource } from "./tools/browse-source.js"
|
|
8
|
+
import { getInstallCommand } from "./tools/get-install-command.js"
|
|
9
|
+
import { install } from "./tools/install.js"
|
|
10
|
+
import { llmContext } from "./tools/llm-context.js"
|
|
11
|
+
import { packageContext } from "./tools/package-context.js"
|
|
12
|
+
import { packageInfo } from "./tools/package-info.js"
|
|
13
|
+
import { packageSkills } from "./tools/package-skills.js"
|
|
14
|
+
import { packagesByOwner } from "./tools/packages-by-owner.js"
|
|
15
|
+
import { poolStats } from "./tools/pool-stats.js"
|
|
16
|
+
import { qualityReport } from "./tools/quality-report.js"
|
|
17
|
+
import { search } from "./tools/search.js"
|
|
18
|
+
import { searchOwners } from "./tools/search-owners.js"
|
|
19
|
+
import { userInfo } from "./tools/user-info.js"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create and configure the MCP server with all LPM tools.
|
|
23
|
+
* @returns {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer}
|
|
24
|
+
*/
|
|
25
|
+
export function createServer() {
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: "lpm-registry",
|
|
28
|
+
version: "0.1.0",
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const cache = new MemoryCache()
|
|
32
|
+
const context = { cache, getToken, getBaseUrl }
|
|
33
|
+
|
|
34
|
+
server.tool(
|
|
35
|
+
"lpm_package_info",
|
|
36
|
+
"Get metadata for an LPM package including versions, description, downloads, AI analysis, compatibility, and readme",
|
|
37
|
+
{
|
|
38
|
+
name: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe(
|
|
41
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
params => packageInfo(params, context),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
server.tool(
|
|
48
|
+
"lpm_quality_report",
|
|
49
|
+
"Get the quality score and detailed check breakdown for an LPM package (27 checks across documentation, code, testing, health)",
|
|
50
|
+
{
|
|
51
|
+
name: z.string().describe("Package name in owner.package-name format"),
|
|
52
|
+
},
|
|
53
|
+
params => qualityReport(params, context),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
server.tool(
|
|
57
|
+
"lpm_api_docs",
|
|
58
|
+
"Get structured API documentation for an LPM package — functions, classes, interfaces, type aliases, enums, and variables with signatures, params, return types, and descriptions. Use this to understand how to use a package before installing it.",
|
|
59
|
+
{
|
|
60
|
+
name: z
|
|
61
|
+
.string()
|
|
62
|
+
.describe(
|
|
63
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
64
|
+
),
|
|
65
|
+
version: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Specific version to get docs for (defaults to latest)"),
|
|
69
|
+
},
|
|
70
|
+
params => apiDocs(params, context),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
"lpm_llm_context",
|
|
75
|
+
"Get an LLM-optimized usage guide for an LPM package — purpose, quickStart code, key exports with signatures, common usage patterns, gotchas, and when to use it. Use this to quickly understand how to use a package correctly.",
|
|
76
|
+
{
|
|
77
|
+
name: z
|
|
78
|
+
.string()
|
|
79
|
+
.describe(
|
|
80
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
81
|
+
),
|
|
82
|
+
version: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Specific version to get context for (defaults to latest)"),
|
|
86
|
+
},
|
|
87
|
+
params => llmContext(params, context),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
server.tool(
|
|
91
|
+
"lpm_package_context",
|
|
92
|
+
"Get complete context for an LPM package in a single call — combines condensed package metadata (name, version, description, install method, dependencies), structured API docs (functions, classes, types), and LLM usage guide (quickStart, patterns, gotchas). Use this as the primary tool when you need to understand and use a package.",
|
|
93
|
+
{
|
|
94
|
+
name: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe(
|
|
97
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
98
|
+
),
|
|
99
|
+
version: z
|
|
100
|
+
.string()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Specific version to get context for (defaults to latest)"),
|
|
103
|
+
},
|
|
104
|
+
params => packageContext(params, context),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
server.tool(
|
|
108
|
+
"lpm_package_skills",
|
|
109
|
+
"Get author-written Agent Skills for an LPM package - usage patterns, anti-patterns, gotchas, and best practices. Skills are version-specific and automatically resolve from local package.json if no version is specified.",
|
|
110
|
+
{
|
|
111
|
+
name: z
|
|
112
|
+
.string()
|
|
113
|
+
.describe(
|
|
114
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
115
|
+
),
|
|
116
|
+
version: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe(
|
|
120
|
+
"Specific version to get skills for (defaults to version in local package.json, then latest)",
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
params => packageSkills(params, context),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
"lpm_pool_stats",
|
|
128
|
+
"Get your Pool revenue sharing earnings estimate for the current month. Shows per-package breakdown with installs, share %, and estimated earnings. Requires authentication.",
|
|
129
|
+
{},
|
|
130
|
+
params => poolStats(params, context),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
server.tool(
|
|
134
|
+
"lpm_search_owners",
|
|
135
|
+
"Search for users or organizations on the LPM registry by name or username.",
|
|
136
|
+
{
|
|
137
|
+
query: z.string().describe("Name or username to search for"),
|
|
138
|
+
limit: z
|
|
139
|
+
.number()
|
|
140
|
+
.min(1)
|
|
141
|
+
.max(10)
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Maximum number of results (1-10, default 5)"),
|
|
144
|
+
},
|
|
145
|
+
params => searchOwners(params, context),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
server.tool(
|
|
149
|
+
"lpm_packages_by_owner",
|
|
150
|
+
"List packages published by a specific user or organization on the LPM registry. Shows public packages with distribution mode.",
|
|
151
|
+
{
|
|
152
|
+
owner: z
|
|
153
|
+
.string()
|
|
154
|
+
.describe("Username or organization slug to list packages for"),
|
|
155
|
+
limit: z
|
|
156
|
+
.number()
|
|
157
|
+
.min(1)
|
|
158
|
+
.max(50)
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Maximum number of results (1-50, default 10)"),
|
|
161
|
+
},
|
|
162
|
+
params => packagesByOwner(params, context),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
server.tool(
|
|
166
|
+
"lpm_user_info",
|
|
167
|
+
"Get information about the authenticated LPM user including organizations, plan tier, pool access, and usage limits. Requires authentication.",
|
|
168
|
+
{},
|
|
169
|
+
params => userInfo(params, context),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
server.tool(
|
|
173
|
+
"lpm_get_install_command",
|
|
174
|
+
"Get the correct CLI command to install an LPM package. Returns either `lpm add` (extracts source files for customization — components, blocks, Swift packages) or `lpm install` (installs to node_modules as a dependency — libraries, utilities).",
|
|
175
|
+
{
|
|
176
|
+
name: z
|
|
177
|
+
.string()
|
|
178
|
+
.describe(
|
|
179
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
180
|
+
),
|
|
181
|
+
version: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe("Specific version to install (defaults to latest)"),
|
|
185
|
+
},
|
|
186
|
+
params => getInstallCommand(params, context),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
server.tool(
|
|
190
|
+
"lpm_add",
|
|
191
|
+
"Add an LPM package to the project by extracting source files for customization. Use for UI components, blocks, templates, Swift packages, and MCP servers. Requires LPM CLI installed.",
|
|
192
|
+
{
|
|
193
|
+
name: z
|
|
194
|
+
.string()
|
|
195
|
+
.describe(
|
|
196
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
197
|
+
),
|
|
198
|
+
version: z
|
|
199
|
+
.string()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Specific version to install (defaults to latest)"),
|
|
202
|
+
path: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe(
|
|
206
|
+
"Target directory for installation (e.g., src/components/ui)",
|
|
207
|
+
),
|
|
208
|
+
alias: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe("Import alias prefix for rewriting (e.g., @/components/ui)"),
|
|
212
|
+
target: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe("Swift SPM target name (for Swift packages)"),
|
|
216
|
+
force: z
|
|
217
|
+
.boolean()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("Overwrite existing files without prompting"),
|
|
220
|
+
installDeps: z
|
|
221
|
+
.boolean()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe(
|
|
224
|
+
"Auto-install npm dependencies (default: true, set false to skip)",
|
|
225
|
+
),
|
|
226
|
+
config: z
|
|
227
|
+
.record(z.string())
|
|
228
|
+
.optional()
|
|
229
|
+
.describe(
|
|
230
|
+
'Config schema params as key-value pairs (e.g., { "styling": "panda" })',
|
|
231
|
+
),
|
|
232
|
+
},
|
|
233
|
+
params => add(params, context),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
server.tool(
|
|
237
|
+
"lpm_install",
|
|
238
|
+
"Install an LPM package as a dependency to node_modules (like npm install). Use for JS libraries, utilities, and SDKs. Requires LPM CLI installed.",
|
|
239
|
+
{
|
|
240
|
+
name: z
|
|
241
|
+
.string()
|
|
242
|
+
.describe(
|
|
243
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
244
|
+
),
|
|
245
|
+
version: z
|
|
246
|
+
.string()
|
|
247
|
+
.optional()
|
|
248
|
+
.describe("Specific version to install (defaults to latest)"),
|
|
249
|
+
},
|
|
250
|
+
params => install(params, context),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
server.tool(
|
|
254
|
+
"lpm_browse_source",
|
|
255
|
+
`Browse source code of an LPM package you have access to. Use without \`path\` to see the file tree first, then request specific files or directories with \`path\`. Requires authentication. Pool packages require Pool subscription, marketplace packages require a license.
|
|
256
|
+
|
|
257
|
+
IMPORTANT: Prefer fetching directory paths (e.g., "src", "components") to get all files under that directory in a single request, rather than requesting individual files one by one. To get ALL source files at once, pass an empty string "" as path.`,
|
|
258
|
+
{
|
|
259
|
+
name: z
|
|
260
|
+
.string()
|
|
261
|
+
.describe(
|
|
262
|
+
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
263
|
+
),
|
|
264
|
+
version: z
|
|
265
|
+
.string()
|
|
266
|
+
.optional()
|
|
267
|
+
.describe("Specific version to browse (defaults to latest)"),
|
|
268
|
+
path: z
|
|
269
|
+
.string()
|
|
270
|
+
.optional()
|
|
271
|
+
.describe(
|
|
272
|
+
'File or directory path to browse (e.g., "src/index.js" or "src"). Omit to get file tree only. Pass empty string "" to get ALL source files at once.',
|
|
273
|
+
),
|
|
274
|
+
},
|
|
275
|
+
params => browseSource(params, context),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
server.tool(
|
|
279
|
+
"lpm_search",
|
|
280
|
+
"Search LPM packages using natural language or structured filters. Uses hybrid semantic search for natural language queries. Supports filtering by category, distribution mode, package type, ecosystem, license, and more.",
|
|
281
|
+
{
|
|
282
|
+
query: z
|
|
283
|
+
.string()
|
|
284
|
+
.optional()
|
|
285
|
+
.describe(
|
|
286
|
+
'Natural language search query (e.g., "validate user input", "react component library")',
|
|
287
|
+
),
|
|
288
|
+
category: z
|
|
289
|
+
.string()
|
|
290
|
+
.optional()
|
|
291
|
+
.describe("Package category to filter by (e.g., ui-components, tools)"),
|
|
292
|
+
ecosystem: z
|
|
293
|
+
.enum(["js", "swift", "xcframework"])
|
|
294
|
+
.optional()
|
|
295
|
+
.describe(
|
|
296
|
+
'Filter by package ecosystem (e.g., "swift" for iOS/macOS packages)',
|
|
297
|
+
),
|
|
298
|
+
distribution: z
|
|
299
|
+
.enum(["marketplace", "pool", "private"])
|
|
300
|
+
.optional()
|
|
301
|
+
.describe("Filter by distribution mode"),
|
|
302
|
+
packageType: z
|
|
303
|
+
.enum([
|
|
304
|
+
"package",
|
|
305
|
+
"source",
|
|
306
|
+
"mcp-server",
|
|
307
|
+
"vscode-extension",
|
|
308
|
+
"cursor-rules",
|
|
309
|
+
"github-action",
|
|
310
|
+
"xcframework",
|
|
311
|
+
"other",
|
|
312
|
+
])
|
|
313
|
+
.optional()
|
|
314
|
+
.describe("Filter by package type"),
|
|
315
|
+
sort: z
|
|
316
|
+
.enum(["newest", "popular", "name"])
|
|
317
|
+
.optional()
|
|
318
|
+
.describe(
|
|
319
|
+
"Sort order (default: relevance for search, newest for filtered)",
|
|
320
|
+
),
|
|
321
|
+
hasTypes: z
|
|
322
|
+
.boolean()
|
|
323
|
+
.optional()
|
|
324
|
+
.describe("Filter to packages with TypeScript type definitions"),
|
|
325
|
+
moduleType: z
|
|
326
|
+
.enum(["esm", "cjs", "dual"])
|
|
327
|
+
.optional()
|
|
328
|
+
.describe("Filter by JavaScript module type"),
|
|
329
|
+
license: z
|
|
330
|
+
.enum([
|
|
331
|
+
"MIT",
|
|
332
|
+
"Apache-2.0",
|
|
333
|
+
"ISC",
|
|
334
|
+
"GPL-3.0",
|
|
335
|
+
"BSD-3-Clause",
|
|
336
|
+
"Unlicense",
|
|
337
|
+
])
|
|
338
|
+
.optional()
|
|
339
|
+
.describe("Filter by license"),
|
|
340
|
+
minNodeVersion: z
|
|
341
|
+
.enum(["18", "20", "22"])
|
|
342
|
+
.optional()
|
|
343
|
+
.describe(
|
|
344
|
+
"Filter to packages supporting this Node.js version or lower",
|
|
345
|
+
),
|
|
346
|
+
limit: z
|
|
347
|
+
.number()
|
|
348
|
+
.min(1)
|
|
349
|
+
.max(50)
|
|
350
|
+
.optional()
|
|
351
|
+
.describe("Maximum number of results (1-50, default 10)"),
|
|
352
|
+
},
|
|
353
|
+
params => search(params, context),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return server
|
|
357
|
+
}
|
package/lib/tools/add.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { runCli } from "../cli.js"
|
|
2
|
+
import { errorResponse, parseName, textResponse } from "../format.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Add an LPM package to the user's project via the CLI.
|
|
6
|
+
* Extracts source files into the project for customization.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ name: string, version?: string, path?: string, alias?: string, target?: string, force?: boolean, installDeps?: boolean, config?: Record<string, string> }} params
|
|
9
|
+
* @param {{ getToken: () => Promise<string|null> }} ctx
|
|
10
|
+
*/
|
|
11
|
+
export async function add(
|
|
12
|
+
{ name, version, path, alias, target, force, installDeps, config },
|
|
13
|
+
ctx,
|
|
14
|
+
) {
|
|
15
|
+
let owner, pkgName
|
|
16
|
+
try {
|
|
17
|
+
const parsed = parseName(name)
|
|
18
|
+
owner = parsed.owner
|
|
19
|
+
pkgName = parsed.name
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return errorResponse(err.message)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const token = await ctx.getToken()
|
|
25
|
+
if (!token) {
|
|
26
|
+
return errorResponse(
|
|
27
|
+
"Authentication required. Set LPM_TOKEN environment variable or run `lpm login`.",
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Build package reference with optional config params as URL query string
|
|
32
|
+
let pkgRef = `@lpm.dev/${owner}.${pkgName}`
|
|
33
|
+
if (version) pkgRef += `@${version}`
|
|
34
|
+
|
|
35
|
+
// Append config params as inline config (e.g., ?styling=panda&component=dialog)
|
|
36
|
+
if (config && typeof config === "object" && Object.keys(config).length > 0) {
|
|
37
|
+
const params = new URLSearchParams(config)
|
|
38
|
+
pkgRef += `?${params.toString()}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build CLI args
|
|
42
|
+
const args = ["add", pkgRef, "--yes", "--json"]
|
|
43
|
+
|
|
44
|
+
if (path) args.push("--path", path)
|
|
45
|
+
if (alias) args.push("--alias", alias)
|
|
46
|
+
if (target) args.push("--target", target)
|
|
47
|
+
if (force) args.push("--force")
|
|
48
|
+
if (installDeps === false) args.push("--no-install-deps")
|
|
49
|
+
|
|
50
|
+
const result = await runCli(args)
|
|
51
|
+
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
return errorResponse(result.error || "Failed to add package.")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Format the result for the AI
|
|
57
|
+
const data = result.data
|
|
58
|
+
const summary = {
|
|
59
|
+
success: true,
|
|
60
|
+
package: data.package,
|
|
61
|
+
installPath: data.installPath,
|
|
62
|
+
alias: data.alias || null,
|
|
63
|
+
files: (data.files || []).map(f => ({
|
|
64
|
+
dest: f.dest,
|
|
65
|
+
action: f.action,
|
|
66
|
+
})),
|
|
67
|
+
dependencies: data.dependencies || { npm: [], lpm: [] },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (data.config && Object.keys(data.config).length > 0) {
|
|
71
|
+
summary.config = data.config
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (data.warnings?.length > 0) {
|
|
75
|
+
summary.warnings = data.warnings
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return textResponse(JSON.stringify(summary, null, 2))
|
|
79
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { registryGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL } from "../constants.js"
|
|
3
|
+
import { errorResponse, parseName, textResponse } from "../format.js"
|
|
4
|
+
import { resolveInstalledVersion } from "../resolve-version.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetch the structured API documentation for an LPM package.
|
|
8
|
+
* Returns functions, interfaces, classes, type aliases, enums, and variables
|
|
9
|
+
* with signatures, params, return types, and descriptions.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ name: string, version?: string }} params
|
|
12
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
13
|
+
*/
|
|
14
|
+
export async function apiDocs({ name, version }, ctx) {
|
|
15
|
+
let owner, pkgName
|
|
16
|
+
try {
|
|
17
|
+
const parsed = parseName(name)
|
|
18
|
+
owner = parsed.owner
|
|
19
|
+
pkgName = parsed.name
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return errorResponse(err.message)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const token = await ctx.getToken()
|
|
25
|
+
const queryName = `${owner}.${pkgName}`
|
|
26
|
+
|
|
27
|
+
// Resolve version from local package.json if not specified
|
|
28
|
+
const resolvedVersion =
|
|
29
|
+
version || resolveInstalledVersion(queryName) || undefined
|
|
30
|
+
const versionKey = resolvedVersion || "latest"
|
|
31
|
+
const cacheKey = `api-docs:${queryName}:${versionKey}:auth=${!!token}`
|
|
32
|
+
|
|
33
|
+
const cached = ctx.cache.get(cacheKey)
|
|
34
|
+
if (cached) return cached
|
|
35
|
+
|
|
36
|
+
const baseUrl = ctx.getBaseUrl()
|
|
37
|
+
const params = new URLSearchParams({ name: queryName })
|
|
38
|
+
if (resolvedVersion) params.set("version", resolvedVersion)
|
|
39
|
+
|
|
40
|
+
const result = await registryGet(
|
|
41
|
+
`/api-docs?${params.toString()}`,
|
|
42
|
+
token,
|
|
43
|
+
baseUrl,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
if (result.status === 404) {
|
|
48
|
+
const label = resolvedVersion
|
|
49
|
+
? `${queryName}@${resolvedVersion}`
|
|
50
|
+
: queryName
|
|
51
|
+
return errorResponse(`Package ${label} not found.`)
|
|
52
|
+
}
|
|
53
|
+
if (result.status === 403) {
|
|
54
|
+
return errorResponse(
|
|
55
|
+
"Access denied. Private packages require authentication from the package owner.",
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return errorResponse(
|
|
59
|
+
result.data?.error || `Request failed (${result.status})`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = result.data
|
|
64
|
+
|
|
65
|
+
// If docs aren't available yet, return a helpful message
|
|
66
|
+
if (!data.available) {
|
|
67
|
+
const response = textResponse(
|
|
68
|
+
`API docs for ${data.name}@${data.version}: ${data.message}`,
|
|
69
|
+
)
|
|
70
|
+
// Cache unavailable status for a shorter time (1 min) so retries work
|
|
71
|
+
ctx.cache.set(cacheKey, response, 60_000)
|
|
72
|
+
return response
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const response = textResponse(JSON.stringify(data, null, 2))
|
|
76
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
77
|
+
return response
|
|
78
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { registryGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL } from "../constants.js"
|
|
3
|
+
import { errorResponse, parseName, textResponse } from "../format.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Browse source code of an LPM package.
|
|
7
|
+
*
|
|
8
|
+
* Calls the registry source browsing API endpoint:
|
|
9
|
+
* GET /api/registry/@lpm.dev/{owner}.{name}/source?version=...&path=...
|
|
10
|
+
*
|
|
11
|
+
* - Without `path`: returns file tree only (use this first to explore structure)
|
|
12
|
+
* - With `path`: returns file tree + source contents for matching files/directory
|
|
13
|
+
*
|
|
14
|
+
* @param {{ name: string, version?: string, path?: string }} params
|
|
15
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
16
|
+
*/
|
|
17
|
+
export async function browseSource({ name, version, path }, ctx) {
|
|
18
|
+
let owner, pkgName
|
|
19
|
+
try {
|
|
20
|
+
const parsed = parseName(name)
|
|
21
|
+
owner = parsed.owner
|
|
22
|
+
pkgName = parsed.name
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return errorResponse(err.message)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const token = await ctx.getToken()
|
|
28
|
+
if (!token) {
|
|
29
|
+
return errorResponse(
|
|
30
|
+
"Authentication required to browse source code. Set LPM_TOKEN environment variable.",
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Build cache key (includes path for content requests)
|
|
35
|
+
const cacheKey = `browse-source:${owner}.${pkgName}:${version || "latest"}:${path || ""}`
|
|
36
|
+
const cached = ctx.cache.get(cacheKey)
|
|
37
|
+
if (cached) return cached
|
|
38
|
+
|
|
39
|
+
// Build query string
|
|
40
|
+
// path can be "" (all files), a specific path, or undefined (tree only)
|
|
41
|
+
const params = new URLSearchParams()
|
|
42
|
+
if (version) params.set("version", version)
|
|
43
|
+
if (path != null) params.set("path", path)
|
|
44
|
+
const qs = params.toString()
|
|
45
|
+
|
|
46
|
+
const baseUrl = ctx.getBaseUrl()
|
|
47
|
+
const url = `/@lpm.dev/${owner}.${pkgName}/source${qs ? `?${qs}` : ""}`
|
|
48
|
+
const result = await registryGet(url, token, baseUrl)
|
|
49
|
+
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
if (result.status === 404) {
|
|
52
|
+
const versionLabel = version || "latest"
|
|
53
|
+
return errorResponse(
|
|
54
|
+
`Version ${versionLabel} not found for @lpm.dev/${owner}.${pkgName}.`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (result.status === 401) {
|
|
58
|
+
return errorResponse(
|
|
59
|
+
"Authentication failed. Your token may be expired. Run `lpm login` to re-authenticate.",
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
if (result.status === 403) {
|
|
63
|
+
return errorResponse(
|
|
64
|
+
result.data?.error ||
|
|
65
|
+
"Access denied. You may need a Pool subscription or license to browse this package.",
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
if (result.status === 429) {
|
|
69
|
+
return errorResponse("Rate limit exceeded. Wait a minute and try again.")
|
|
70
|
+
}
|
|
71
|
+
if (result.status === 503) {
|
|
72
|
+
return errorResponse("Source browsing is currently disabled.")
|
|
73
|
+
}
|
|
74
|
+
return errorResponse(
|
|
75
|
+
result.data?.error || `Request failed (${result.status})`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = result.data
|
|
80
|
+
|
|
81
|
+
// Format response
|
|
82
|
+
const response = {
|
|
83
|
+
package: data.package,
|
|
84
|
+
version: data.version,
|
|
85
|
+
ecosystem: data.ecosystem,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Always include tree
|
|
89
|
+
response.tree = data.tree || []
|
|
90
|
+
|
|
91
|
+
// Include files if present (only when path was requested)
|
|
92
|
+
if (data.files && data.files.length > 0) {
|
|
93
|
+
response.files = data.files
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Include truncation warning
|
|
97
|
+
if (data.truncated) {
|
|
98
|
+
response.truncated = true
|
|
99
|
+
response.warning =
|
|
100
|
+
"Response was truncated due to size limits. Use the `path` parameter to request specific files or directories."
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Include package config if present (helps AI understand package structure)
|
|
104
|
+
if (data.lpmConfig) {
|
|
105
|
+
response.lpmConfig = data.lpmConfig
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const mcpResponse = textResponse(JSON.stringify(response, null, 2))
|
|
109
|
+
ctx.cache.set(cacheKey, mcpResponse, CACHE_TTL.SHORT)
|
|
110
|
+
return mcpResponse
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { registryGet } from "../api.js"
|
|
2
|
+
import { CACHE_TTL } from "../constants.js"
|
|
3
|
+
import { errorResponse, parseName, textResponse } from "../format.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the correct CLI install command for an LPM package.
|
|
7
|
+
* Returns `lpm add` for source packages (components, blocks, MCP servers, Swift)
|
|
8
|
+
* or `lpm install` for dependency packages (installed to node_modules).
|
|
9
|
+
*
|
|
10
|
+
* @param {{ name: string, version?: string }} params
|
|
11
|
+
* @param {{ cache: import('../cache.js').MemoryCache, getToken: () => Promise<string|null>, getBaseUrl: () => string }} ctx
|
|
12
|
+
*/
|
|
13
|
+
export async function getInstallCommand({ name, version }, ctx) {
|
|
14
|
+
let owner, pkgName
|
|
15
|
+
try {
|
|
16
|
+
const parsed = parseName(name)
|
|
17
|
+
owner = parsed.owner
|
|
18
|
+
pkgName = parsed.name
|
|
19
|
+
} catch (err) {
|
|
20
|
+
return errorResponse(err.message)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fullName = `@lpm.dev/${owner}.${pkgName}`
|
|
24
|
+
const versionSuffix = version ? `@${version}` : ""
|
|
25
|
+
|
|
26
|
+
const token = await ctx.getToken()
|
|
27
|
+
const cacheKey = `install-cmd:${owner}.${pkgName}:auth=${!!token}`
|
|
28
|
+
|
|
29
|
+
const cached = ctx.cache.get(cacheKey)
|
|
30
|
+
if (cached) return cached
|
|
31
|
+
|
|
32
|
+
const baseUrl = ctx.getBaseUrl()
|
|
33
|
+
const result = await registryGet(`/${fullName}`, token, baseUrl)
|
|
34
|
+
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
// Fallback: return `lpm add` as the default command when metadata is unavailable
|
|
37
|
+
if (result.status === 404) {
|
|
38
|
+
return errorResponse(`Package ${fullName} not found.`)
|
|
39
|
+
}
|
|
40
|
+
const response = textResponse(`lpm add ${fullName}${versionSuffix}`)
|
|
41
|
+
return response
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = result.data
|
|
45
|
+
const ecosystem = data.ecosystem || "js"
|
|
46
|
+
const packageType = data.packageType || "package"
|
|
47
|
+
|
|
48
|
+
// Source-based packages use `lpm add` (extracts files into the project)
|
|
49
|
+
// Only plain JS packages without a special type use `lpm install` (node_modules)
|
|
50
|
+
const useAdd =
|
|
51
|
+
ecosystem !== "js" ||
|
|
52
|
+
packageType !== "package" ||
|
|
53
|
+
data.lpmSource ||
|
|
54
|
+
data.hasLpmConfig
|
|
55
|
+
|
|
56
|
+
const command = useAdd
|
|
57
|
+
? `lpm add ${fullName}${versionSuffix}`
|
|
58
|
+
: `lpm install ${fullName}${versionSuffix}`
|
|
59
|
+
|
|
60
|
+
const explanation = useAdd
|
|
61
|
+
? "Extracts source files into your project for customization."
|
|
62
|
+
: "Installs as a dependency in node_modules (like npm install)."
|
|
63
|
+
|
|
64
|
+
const response = textResponse(
|
|
65
|
+
JSON.stringify(
|
|
66
|
+
{ command, method: useAdd ? "add" : "install", explanation },
|
|
67
|
+
null,
|
|
68
|
+
2,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
ctx.cache.set(cacheKey, response, CACHE_TTL.SHORT)
|
|
72
|
+
return response
|
|
73
|
+
}
|