@lpm-registry/mcp-server 0.2.0 → 0.3.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 +24 -613
- package/lib/auth.js +104 -12
- package/lib/cli.js +82 -29
- package/lib/constants.js +2 -2
- package/lib/format.js +8 -0
- package/lib/server.js +7 -4
- package/lib/tools/add.js +12 -40
- package/lib/tools/audit.js +3 -34
- package/lib/tools/install.js +3 -26
- package/lib/tools/search.js +14 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ lpm mcp setup
|
|
|
12
12
|
|
|
13
13
|
This auto-detects Claude Code, Cursor, VS Code, Claude Desktop, and Windsurf, then writes the correct config to each. Authentication is handled via `lpm login` (stored in your OS keychain) — no tokens in config files.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The generated command is `npx -y @lpm-registry/mcp-server@latest`. The `latest` dist-tag is resolved when the editor starts, so editor restarts pick up the current published MCP server release.
|
|
16
|
+
|
|
17
|
+
For a custom registry, set `LPM_REGISTRY_URL` in the environment inherited by your editor or add it to the editor's MCP server environment configuration.
|
|
16
18
|
|
|
17
19
|
## Manual Setup
|
|
18
20
|
|
|
@@ -72,9 +74,9 @@ If you prefer manual configuration, add to your editor's MCP config:
|
|
|
72
74
|
|
|
73
75
|
## Authentication
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
`LPM_TOKEN` has highest priority. When it is unset, the server reads the OS keychain entry created by `lpm login` for the effective registry URL. Credentials are registry-scoped, so hosted, local, and custom registries do not reuse one another's tokens.
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
Set `LPM_TOKEN` explicitly for environments without keychain access:
|
|
78
80
|
|
|
79
81
|
```bash
|
|
80
82
|
export LPM_TOKEN=lpm_your_token_here
|
|
@@ -92,40 +94,25 @@ For local development, copy `.env.example` to `.env.local` and set your local re
|
|
|
92
94
|
|
|
93
95
|
## Available Tools
|
|
94
96
|
|
|
95
|
-
| Tool
|
|
96
|
-
|
|
|
97
|
-
| `lpm_search`
|
|
98
|
-
| `lpm_package_info`
|
|
99
|
-
| `lpm_api_docs`
|
|
100
|
-
| `lpm_llm_context`
|
|
101
|
-
| `lpm_package_context`
|
|
102
|
-
| `lpm_package_skills`
|
|
103
|
-
| `lpm_docs`
|
|
104
|
-
| `lpm_browse_source`
|
|
105
|
-
| `lpm_add`
|
|
106
|
-
| `lpm_install`
|
|
107
|
-
| `lpm_audit`
|
|
108
|
-
| `lpm_marketplace_info`
|
|
109
|
-
| `lpm_quality_report`
|
|
110
|
-
| `lpm_search_owners`
|
|
111
|
-
| `lpm_packages_by_owner`
|
|
112
|
-
| `lpm_pool_stats`
|
|
113
|
-
| `lpm_user_info`
|
|
114
|
-
|
|
115
|
-
## AI Workflow
|
|
116
|
-
|
|
117
|
-
A typical AI agent workflow for finding and adding a package:
|
|
118
|
-
|
|
119
|
-
1. **Search** — `lpm_search` to find packages matching the need
|
|
120
|
-
2. **Evaluate** — `lpm_package_context` to understand the package before installing (metadata, API docs, usage guide)
|
|
121
|
-
3. **Install** — `lpm_install` for managed dependencies (JS + Swift) or `lpm_add` for source components
|
|
122
|
-
4. **Build** — `lpm_package_skills` for usage patterns, then read local files directly
|
|
123
|
-
|
|
124
|
-
The `lpm_package_context` response includes `package.installMethod.command` (`lpm add` or `lpm install`) so agents know which tool to use.
|
|
125
|
-
|
|
126
|
-
> **Tip:** After installing, prefer reading local files over `lpm_browse_source`. Local reads are faster and don't hit the server. Use `lpm_browse_source` only when evaluating a package you can't install (e.g., checking before purchase).
|
|
127
|
-
|
|
128
|
-
> **Version resolution:** Version-sensitive tools (`lpm_api_docs`, `lpm_llm_context`, `lpm_package_context`, `lpm_package_skills`, `lpm_browse_source`) resolve the version from the local project's package.json dependencies when no explicit version is specified. This ensures you get docs matching the version you actually have installed.
|
|
97
|
+
| Tool | Description | Auth | Cache |
|
|
98
|
+
| ----------------------- | ----------------------------------------------------------------------- | -------- | ----- |
|
|
99
|
+
| `lpm_search` | Search packages with natural language or structured filters | Optional | 5m |
|
|
100
|
+
| `lpm_package_info` | Get package metadata, install method, access model, and readme | Optional | 5m |
|
|
101
|
+
| `lpm_api_docs` | Get structured API docs — functions, classes, types, signatures | Optional | 5m |
|
|
102
|
+
| `lpm_llm_context` | Get LLM-optimized usage guide — quickStart, patterns, gotchas | Optional | 5m |
|
|
103
|
+
| `lpm_package_context` | Get complete package context in one call (pre-install evaluation) | Optional | 5m |
|
|
104
|
+
| `lpm_package_skills` | Get Agent Skills for building with an installed package | Optional | 5m |
|
|
105
|
+
| `lpm_docs` | Search or read LPM documentation (setup, CLI, publishing, etc.) | No | 30m |
|
|
106
|
+
| `lpm_browse_source` | Browse package source code remotely (last resort) | Yes | 5m |
|
|
107
|
+
| `lpm_add` | Add a package by extracting source files into the project | Yes | — |
|
|
108
|
+
| `lpm_install` | Install a managed dependency (JS → node_modules, Swift → Package.swift) | Yes | — |
|
|
109
|
+
| `lpm_audit` | Security audit — behavioral tags, AI findings, quality scores | Yes | — |
|
|
110
|
+
| `lpm_marketplace_info` | Marketplace pricing, licensing, and seat management | Optional | 5m |
|
|
111
|
+
| `lpm_quality_report` | Get quality score and 28-check breakdown | Optional | 5m |
|
|
112
|
+
| `lpm_search_owners` | Search for users or organizations by name | No | 5m |
|
|
113
|
+
| `lpm_packages_by_owner` | List packages published by a specific user or org | No | 5m |
|
|
114
|
+
| `lpm_pool_stats` | Get your Pool revenue earnings for the current month | Yes | 1h |
|
|
115
|
+
| `lpm_user_info` | Get authenticated user info, orgs, and usage | Yes | 5m |
|
|
129
116
|
|
|
130
117
|
### Access Control
|
|
131
118
|
|
|
@@ -133,582 +120,6 @@ The `lpm_package_context` response includes `package.installMethod.command` (`lp
|
|
|
133
120
|
- **Marketplace packages** require a license purchase. Errors include a link to the package page.
|
|
134
121
|
- **Public metadata** (`lpm_package_info`, `lpm_search`, `lpm_quality_report`) works without auth for public packages.
|
|
135
122
|
|
|
136
|
-
## Tool Details
|
|
137
|
-
|
|
138
|
-
### lpm_search
|
|
139
|
-
|
|
140
|
-
Search LPM packages using natural language or structured filters. Uses hybrid semantic search for natural language queries, and the explore API when structured filters are active.
|
|
141
|
-
|
|
142
|
-
**Parameters:**
|
|
143
|
-
|
|
144
|
-
- `query` (string, optional) — Natural language search query (required unless `category` is provided)
|
|
145
|
-
- `category` (string, optional) — Package category filter (e.g., `ui-components`, `tools`)
|
|
146
|
-
- `ecosystem` (enum, optional) — Filter by ecosystem: `js`, `swift`, or `xcframework`
|
|
147
|
-
- `distribution` (enum, optional) — Filter by distribution mode: `marketplace`, `pool`, or `private`
|
|
148
|
-
- `packageType` (enum, optional) — Filter by type: `package`, `source`, `mcp-server`, `vscode-extension`, `cursor-rules`, `github-action`, `xcframework`, `other`
|
|
149
|
-
- `sort` (enum, optional) — Sort order: `newest`, `popular`, or `name`
|
|
150
|
-
- `hasTypes` (boolean, optional) — Filter to packages with TypeScript type definitions
|
|
151
|
-
- `moduleType` (enum, optional) — Filter by module type: `esm`, `cjs`, or `dual`
|
|
152
|
-
- `license` (enum, optional) — Filter by license: `MIT`, `Apache-2.0`, `ISC`, `GPL-3.0`, `BSD-3-Clause`, `Unlicense`
|
|
153
|
-
- `minNodeVersion` (enum, optional) — Filter to packages supporting Node.js `18`, `20`, or `22`+
|
|
154
|
-
- `limit` (number, optional) — Max results, 1-50 (default: 10)
|
|
155
|
-
|
|
156
|
-
At least one of `query` or `category` is required.
|
|
157
|
-
|
|
158
|
-
**Example response:**
|
|
159
|
-
|
|
160
|
-
```
|
|
161
|
-
Found 2 packages:
|
|
162
|
-
|
|
163
|
-
- alice.ui-kit [source] (pool) — React UI components (5,000 downloads)
|
|
164
|
-
Quality: 88 | Category: ui-components | Tags: react, components
|
|
165
|
-
- bob.form-builder (marketplace) — Form builder library (1,200 downloads)
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### lpm_package_info
|
|
169
|
-
|
|
170
|
-
Get metadata for an LPM package including versions, description, downloads, install method, access status, and readme.
|
|
171
|
-
|
|
172
|
-
**Parameters:**
|
|
173
|
-
|
|
174
|
-
- `name` (string, required) — Package name in `owner.package-name` or `@lpm.dev/owner.package-name` format
|
|
175
|
-
|
|
176
|
-
**Example response:**
|
|
177
|
-
|
|
178
|
-
```json
|
|
179
|
-
{
|
|
180
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
181
|
-
"description": "A modern UI component kit for React",
|
|
182
|
-
"ecosystem": "js",
|
|
183
|
-
"latestVersion": "2.1.0",
|
|
184
|
-
"totalVersions": 12,
|
|
185
|
-
"versions": ["2.1.0", "2.0.0", "1.9.0"],
|
|
186
|
-
"downloads": 5400,
|
|
187
|
-
"createdAt": "2024-01-15T10:30:00Z",
|
|
188
|
-
"updatedAt": "2025-11-20T14:00:00Z",
|
|
189
|
-
"dependencies": ["react", "react-dom"],
|
|
190
|
-
"readme": "# UI Kit\n\nA modern component library...",
|
|
191
|
-
"distributionMode": "pool",
|
|
192
|
-
"hasAccess": true,
|
|
193
|
-
"installMethod": {
|
|
194
|
-
"command": "lpm add",
|
|
195
|
-
"description": "Extracts source files into your project for customization."
|
|
196
|
-
},
|
|
197
|
-
"accessInfo": {
|
|
198
|
-
"model": "pool",
|
|
199
|
-
"summary": "Included with Pool subscription ($12/mo)",
|
|
200
|
-
"actionRequired": false
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### lpm_api_docs
|
|
206
|
-
|
|
207
|
-
Get structured API documentation for an LPM package. Returns functions, classes, interfaces, type aliases, enums, and variables with full signatures, parameter types, return types, and descriptions. Use this to understand how to use a package before installing it.
|
|
208
|
-
|
|
209
|
-
API docs are auto-generated during publish from TypeScript definitions, AI analysis, or JSDoc annotations.
|
|
210
|
-
|
|
211
|
-
**Parameters:**
|
|
212
|
-
|
|
213
|
-
- `name` (string, required) — Package name in `owner.package-name` or `@lpm.dev/owner.package-name` format
|
|
214
|
-
- `version` (string, optional) — Specific version to get docs for (defaults to latest)
|
|
215
|
-
|
|
216
|
-
**Example response:**
|
|
217
|
-
|
|
218
|
-
```json
|
|
219
|
-
{
|
|
220
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
221
|
-
"version": "2.1.0",
|
|
222
|
-
"available": true,
|
|
223
|
-
"docsStatus": "extracted",
|
|
224
|
-
"apiDocs": {
|
|
225
|
-
"version": 1,
|
|
226
|
-
"strategy": "typescript",
|
|
227
|
-
"entryPoint": "dist/index.d.ts",
|
|
228
|
-
"modules": [
|
|
229
|
-
{
|
|
230
|
-
"path": "index",
|
|
231
|
-
"functions": [
|
|
232
|
-
{
|
|
233
|
-
"name": "createTheme",
|
|
234
|
-
"description": "Create a custom theme configuration",
|
|
235
|
-
"signatures": [
|
|
236
|
-
{
|
|
237
|
-
"params": [
|
|
238
|
-
{ "name": "options", "type": "ThemeOptions", "optional": true }
|
|
239
|
-
],
|
|
240
|
-
"returnType": "Theme",
|
|
241
|
-
"typeParams": []
|
|
242
|
-
}
|
|
243
|
-
]
|
|
244
|
-
}
|
|
245
|
-
],
|
|
246
|
-
"interfaces": [
|
|
247
|
-
{
|
|
248
|
-
"name": "ThemeOptions",
|
|
249
|
-
"description": "Configuration for theme creation",
|
|
250
|
-
"properties": [
|
|
251
|
-
{ "name": "colors", "type": "ColorPalette", "optional": true },
|
|
252
|
-
{ "name": "spacing", "type": "SpacingScale", "optional": true }
|
|
253
|
-
]
|
|
254
|
-
}
|
|
255
|
-
],
|
|
256
|
-
"classes": [],
|
|
257
|
-
"typeAliases": [],
|
|
258
|
-
"enums": [],
|
|
259
|
-
"variables": []
|
|
260
|
-
}
|
|
261
|
-
],
|
|
262
|
-
"stats": {
|
|
263
|
-
"functionCount": 5,
|
|
264
|
-
"classCount": 2,
|
|
265
|
-
"interfaceCount": 8,
|
|
266
|
-
"totalExports": 18
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
**When docs aren't available:**
|
|
273
|
-
|
|
274
|
-
```json
|
|
275
|
-
{
|
|
276
|
-
"name": "@lpm.dev/bob.utils",
|
|
277
|
-
"version": "1.0.0",
|
|
278
|
-
"available": false,
|
|
279
|
-
"docsStatus": "pending",
|
|
280
|
-
"message": "API docs are being generated. Try again in a few minutes."
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### lpm_llm_context
|
|
285
|
-
|
|
286
|
-
Get an LLM-optimized usage guide for an LPM package. Returns a concise cheat sheet with purpose, quickStart code, key exports with signatures, common usage patterns, gotchas, and guidance on when to use the package. Use this to quickly understand how to write correct code with a package.
|
|
287
|
-
|
|
288
|
-
LLM context is auto-generated during publish using AI analysis of the package's API docs, source code, and readme.
|
|
289
|
-
|
|
290
|
-
**Parameters:**
|
|
291
|
-
|
|
292
|
-
- `name` (string, required) — Package name in `owner.package-name` or `@lpm.dev/owner.package-name` format
|
|
293
|
-
- `version` (string, optional) — Specific version to get context for (defaults to latest)
|
|
294
|
-
|
|
295
|
-
**Example response:**
|
|
296
|
-
|
|
297
|
-
```json
|
|
298
|
-
{
|
|
299
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
300
|
-
"version": "2.1.0",
|
|
301
|
-
"available": true,
|
|
302
|
-
"llmContextStatus": "extracted",
|
|
303
|
-
"llmContext": {
|
|
304
|
-
"version": 1,
|
|
305
|
-
"purpose": "React UI component library with theme support and accessibility built-in",
|
|
306
|
-
"quickStart": "import { Button, Input } from '@lpm.dev/alice.ui-kit'\n\n<Button variant=\"primary\">Click me</Button>",
|
|
307
|
-
"keyExports": [
|
|
308
|
-
{
|
|
309
|
-
"name": "Button",
|
|
310
|
-
"kind": "component",
|
|
311
|
-
"signature": "(props: ButtonProps) => JSX.Element",
|
|
312
|
-
"description": "Primary button component with variants and loading state"
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
"name": "createTheme",
|
|
316
|
-
"kind": "function",
|
|
317
|
-
"signature": "(options?: ThemeOptions) => Theme",
|
|
318
|
-
"description": "Create a custom theme configuration"
|
|
319
|
-
}
|
|
320
|
-
],
|
|
321
|
-
"commonPatterns": [
|
|
322
|
-
{
|
|
323
|
-
"title": "Basic button with loading state",
|
|
324
|
-
"code": "<Button isLoading={isPending} onClick={handleSubmit}>Save</Button>",
|
|
325
|
-
"description": "Use isLoading prop to show spinner during async operations"
|
|
326
|
-
}
|
|
327
|
-
],
|
|
328
|
-
"gotchas": [
|
|
329
|
-
"Requires React 18+ as peer dependency",
|
|
330
|
-
"Theme must be wrapped in ThemeProvider at app root"
|
|
331
|
-
],
|
|
332
|
-
"whenToUse": "Building React apps that need consistent, accessible UI components with theme support",
|
|
333
|
-
"whenNotToUse": "Server-rendered pages without React, or when you need only a single component"
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
**When context isn't available:**
|
|
339
|
-
|
|
340
|
-
```json
|
|
341
|
-
{
|
|
342
|
-
"name": "@lpm.dev/bob.utils",
|
|
343
|
-
"version": "1.0.0",
|
|
344
|
-
"available": false,
|
|
345
|
-
"llmContextStatus": "pending",
|
|
346
|
-
"message": "LLM context is being generated. Try again in a few minutes."
|
|
347
|
-
}
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### lpm_package_context
|
|
351
|
-
|
|
352
|
-
Get complete context for an LPM package in a single call. Combines condensed package metadata, structured API documentation, LLM-optimized usage guide, and Agent Skills. This is the recommended tool when you need to understand a package before using it.
|
|
353
|
-
|
|
354
|
-
Internally makes 4 parallel API calls. If API docs or LLM context aren't available yet (still being generated), they are omitted from the response. Only fails if the package itself is not found or inaccessible.
|
|
355
|
-
|
|
356
|
-
**Parameters:**
|
|
357
|
-
|
|
358
|
-
- `name` (string, required) — Package name in `owner.package-name` or `@lpm.dev/owner.package-name` format
|
|
359
|
-
- `version` (string, optional) — Specific version to get context for (defaults to latest)
|
|
360
|
-
|
|
361
|
-
**Example response (all available):**
|
|
362
|
-
|
|
363
|
-
```json
|
|
364
|
-
{
|
|
365
|
-
"package": {
|
|
366
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
367
|
-
"description": "A modern UI component kit for React",
|
|
368
|
-
"ecosystem": "js",
|
|
369
|
-
"latestVersion": "2.1.0",
|
|
370
|
-
"license": "MIT",
|
|
371
|
-
"dependencies": ["react", "react-dom"],
|
|
372
|
-
"peerDependencies": [],
|
|
373
|
-
"installMethod": {
|
|
374
|
-
"command": "lpm add",
|
|
375
|
-
"description": "Extracts source files into your project for customization"
|
|
376
|
-
},
|
|
377
|
-
"distributionMode": "pool",
|
|
378
|
-
"readme": "# UI Kit\n\nA modern component library for building..."
|
|
379
|
-
},
|
|
380
|
-
"apiDocs": {
|
|
381
|
-
"version": 1,
|
|
382
|
-
"strategy": "typescript",
|
|
383
|
-
"modules": [
|
|
384
|
-
{
|
|
385
|
-
"path": "index",
|
|
386
|
-
"functions": [{ "name": "createTheme", "description": "Create a custom theme" }]
|
|
387
|
-
}
|
|
388
|
-
]
|
|
389
|
-
},
|
|
390
|
-
"llmContext": {
|
|
391
|
-
"version": 1,
|
|
392
|
-
"purpose": "React UI component library with theme support",
|
|
393
|
-
"quickStart": "import { Button } from '@lpm.dev/alice.ui-kit'",
|
|
394
|
-
"keyExports": [{ "name": "Button", "kind": "component" }],
|
|
395
|
-
"commonPatterns": [],
|
|
396
|
-
"gotchas": ["Requires React 18+"],
|
|
397
|
-
"whenToUse": "Building React apps with consistent UI"
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
When API docs, LLM context, or skills aren't available, those keys are simply omitted. Only the `package` key is always present. The readme is truncated to ~500 characters — use `lpm_package_info` for the full readme.
|
|
403
|
-
|
|
404
|
-
### lpm_package_skills
|
|
405
|
-
|
|
406
|
-
Fetch Agent Skills for an LPM package. Returns approved skills with their name, description, applicable file globs, and content. Skills are markdown files that provide workflow-specific instructions for AI coding assistants.
|
|
407
|
-
|
|
408
|
-
**Parameters:**
|
|
409
|
-
|
|
410
|
-
- `name` (string, required) — Package name in `owner.package-name` or `@lpm.dev/owner.package-name` format
|
|
411
|
-
- `version` (string, optional) — Specific version to get skills for (defaults to latest)
|
|
412
|
-
|
|
413
|
-
**Example response:**
|
|
414
|
-
|
|
415
|
-
```json
|
|
416
|
-
{
|
|
417
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
418
|
-
"version": "2.1.0",
|
|
419
|
-
"skills": [
|
|
420
|
-
{
|
|
421
|
-
"name": "theming",
|
|
422
|
-
"description": "How to create and apply custom themes",
|
|
423
|
-
"globs": ["**/*.theme.*", "**/*.styles.*"],
|
|
424
|
-
"content": "# Theming\n\nWhen creating a custom theme..."
|
|
425
|
-
}
|
|
426
|
-
]
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### lpm_browse_source
|
|
431
|
-
|
|
432
|
-
Browse source code of an LPM package you have access to. Call without `path` to get the file tree, then request specific files or directories.
|
|
433
|
-
|
|
434
|
-
**Parameters:**
|
|
435
|
-
|
|
436
|
-
- `name` (string, required) — Package name in `owner.package-name` or `@lpm.dev/owner.package-name` format
|
|
437
|
-
- `version` (string, optional) — Specific version to browse (defaults to latest)
|
|
438
|
-
- `path` (string, optional) — File or directory path (e.g., `src/index.js` or `src`). Omit for tree only.
|
|
439
|
-
|
|
440
|
-
**Example response (tree only):**
|
|
441
|
-
|
|
442
|
-
```json
|
|
443
|
-
{
|
|
444
|
-
"package": "alice.ui-kit",
|
|
445
|
-
"version": "2.0.0",
|
|
446
|
-
"ecosystem": "js",
|
|
447
|
-
"tree": ["src/index.js", "src/button.jsx", "src/input.jsx", "package.json"]
|
|
448
|
-
}
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
**Example response (with path):**
|
|
452
|
-
|
|
453
|
-
```json
|
|
454
|
-
{
|
|
455
|
-
"package": "alice.ui-kit",
|
|
456
|
-
"version": "2.0.0",
|
|
457
|
-
"ecosystem": "js",
|
|
458
|
-
"tree": ["src/index.js", "src/button.jsx", "src/input.jsx", "package.json"],
|
|
459
|
-
"files": [
|
|
460
|
-
{ "path": "src/button.jsx", "content": "export function Button() { ... }" }
|
|
461
|
-
]
|
|
462
|
-
}
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
### lpm_add
|
|
466
|
-
|
|
467
|
-
Add an LPM package to the project by extracting source files for customization. Use for UI components, blocks, templates, and MCP servers. Requires LPM CLI installed locally.
|
|
468
|
-
|
|
469
|
-
**Parameters:**
|
|
470
|
-
|
|
471
|
-
- `name` (string, required) — Package name
|
|
472
|
-
- `version` (string, optional) — Specific version (defaults to latest)
|
|
473
|
-
- `path` (string, optional) — Target directory (e.g., `src/components/ui`)
|
|
474
|
-
- `alias` (string, optional) — Import alias prefix (e.g., `@/components/ui`)
|
|
475
|
-
- `target` (string, optional) — Swift SPM target name
|
|
476
|
-
- `force` (boolean, optional) — Overwrite existing files
|
|
477
|
-
- `installDeps` (boolean, optional) — Auto-install npm deps (default: true)
|
|
478
|
-
- `config` (object, optional) — Config schema params (e.g., `{ "styling": "panda" }`)
|
|
479
|
-
|
|
480
|
-
**Example response:**
|
|
481
|
-
|
|
482
|
-
```json
|
|
483
|
-
{
|
|
484
|
-
"success": true,
|
|
485
|
-
"package": { "name": "@lpm.dev/alice.ui-kit", "version": "2.0.0" },
|
|
486
|
-
"installPath": "src/components/ui",
|
|
487
|
-
"files": [
|
|
488
|
-
{ "dest": "src/components/ui/button.jsx", "action": "created" },
|
|
489
|
-
{ "dest": "src/components/ui/input.jsx", "action": "created" }
|
|
490
|
-
],
|
|
491
|
-
"dependencies": { "npm": ["react"], "lpm": [] }
|
|
492
|
-
}
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### lpm_install
|
|
496
|
-
|
|
497
|
-
Install an LPM package as a managed dependency. JS packages go to node_modules, Swift packages edit Package.swift via SE-0292. Requires LPM CLI installed locally.
|
|
498
|
-
|
|
499
|
-
**Parameters:**
|
|
500
|
-
|
|
501
|
-
- `name` (string, required) — Package name
|
|
502
|
-
- `version` (string, optional) — Specific version (defaults to latest)
|
|
503
|
-
|
|
504
|
-
**Example response:**
|
|
505
|
-
|
|
506
|
-
```json
|
|
507
|
-
{
|
|
508
|
-
"success": true,
|
|
509
|
-
"packages": [{ "name": "@lpm.dev/bob.validate" }],
|
|
510
|
-
"npmOutput": "added 1 package in 1s"
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### lpm_audit
|
|
515
|
-
|
|
516
|
-
Run a security audit on the project's LPM dependencies. Returns behavioral tags (eval, childProcess, shell), AI security findings, quality scores, and lifecycle scripts.
|
|
517
|
-
|
|
518
|
-
**Parameters:**
|
|
519
|
-
|
|
520
|
-
- `path` (string, optional) — Project directory to audit (defaults to current directory)
|
|
521
|
-
|
|
522
|
-
**Example response:**
|
|
523
|
-
|
|
524
|
-
```json
|
|
525
|
-
{
|
|
526
|
-
"success": true,
|
|
527
|
-
"totalPackages": 3,
|
|
528
|
-
"packagesWithIssues": 1,
|
|
529
|
-
"packages": [
|
|
530
|
-
{
|
|
531
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
532
|
-
"version": "2.0.0",
|
|
533
|
-
"qualityScore": 85,
|
|
534
|
-
"issues": []
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
"name": "@lpm.dev/bob.exec-tool",
|
|
538
|
-
"version": "1.0.0",
|
|
539
|
-
"qualityScore": 45,
|
|
540
|
-
"issues": ["uses child_process", "[moderate] Executes shell commands with user input"]
|
|
541
|
-
}
|
|
542
|
-
]
|
|
543
|
-
}
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### lpm_marketplace_info
|
|
547
|
-
|
|
548
|
-
Get marketplace information for an LPM package — pricing, licensing, seat management, and purchase status.
|
|
549
|
-
|
|
550
|
-
**Parameters:**
|
|
551
|
-
|
|
552
|
-
- `name` (string, required) — Package name
|
|
553
|
-
|
|
554
|
-
**Example response:**
|
|
555
|
-
|
|
556
|
-
```json
|
|
557
|
-
{
|
|
558
|
-
"name": "@lpm.dev/acme.pro-suite",
|
|
559
|
-
"isMarketplace": true,
|
|
560
|
-
"distributionMode": "marketplace",
|
|
561
|
-
"pricing": { "monthlyPriceCents": 2900, "yearlyPriceCents": 29000 },
|
|
562
|
-
"licenseType": "per-seat",
|
|
563
|
-
"accessInfo": { "hasLicense": true, "seatsUsed": 3, "seatsTotal": 5 }
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### lpm_docs
|
|
568
|
-
|
|
569
|
-
Search or read LPM documentation. Use when the user asks how to use LPM itself.
|
|
570
|
-
|
|
571
|
-
**Parameters:**
|
|
572
|
-
|
|
573
|
-
- `page` (string, optional) — Doc page slug (e.g., `cli/commands`, `packages/swift-registry`)
|
|
574
|
-
- `query` (string, optional) — Search query (e.g., `npmrc`, `CI deployment`)
|
|
575
|
-
|
|
576
|
-
Without parameters: returns the full docs index.
|
|
577
|
-
|
|
578
|
-
### lpm_quality_report
|
|
579
|
-
|
|
580
|
-
Get the quality score and detailed check breakdown for an LPM package. Covers 28 checks across documentation, code quality, testing, and maintenance health.
|
|
581
|
-
|
|
582
|
-
**Parameters:**
|
|
583
|
-
|
|
584
|
-
- `name` (string, required) — Package name in `owner.package-name` format
|
|
585
|
-
|
|
586
|
-
**Example response:**
|
|
587
|
-
|
|
588
|
-
```json
|
|
589
|
-
{
|
|
590
|
-
"name": "@lpm.dev/alice.ui-kit",
|
|
591
|
-
"score": 85,
|
|
592
|
-
"maxScore": 100,
|
|
593
|
-
"tier": "good",
|
|
594
|
-
"categories": [
|
|
595
|
-
{ "name": "documentation", "score": 22, "maxScore": 25 },
|
|
596
|
-
{ "name": "code", "score": 28, "maxScore": 30 }
|
|
597
|
-
],
|
|
598
|
-
"checks": [
|
|
599
|
-
{ "id": "has-readme", "passed": true, "points": 10, "maxPoints": 10 }
|
|
600
|
-
]
|
|
601
|
-
}
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
### lpm_search_owners
|
|
605
|
-
|
|
606
|
-
Search for users or organizations on the LPM registry by name or username.
|
|
607
|
-
|
|
608
|
-
**Parameters:**
|
|
609
|
-
|
|
610
|
-
- `query` (string, required) — Name or username to search for
|
|
611
|
-
- `limit` (number, optional) — Max results, 1-10 (default: 5)
|
|
612
|
-
|
|
613
|
-
**Example response:**
|
|
614
|
-
|
|
615
|
-
```
|
|
616
|
-
Found 2 profiles:
|
|
617
|
-
|
|
618
|
-
- @alice (Alice Smith) [user] — Full-stack developer
|
|
619
|
-
- @acme (Acme Corp) [org] — Open source tools for developers
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
### lpm_packages_by_owner
|
|
623
|
-
|
|
624
|
-
List packages published by a specific user or organization. Shows only public (pool/marketplace) packages.
|
|
625
|
-
|
|
626
|
-
**Parameters:**
|
|
627
|
-
|
|
628
|
-
- `owner` (string, required) — Username or organization slug
|
|
629
|
-
- `limit` (number, optional) — Max results, 1-50 (default: 10)
|
|
630
|
-
|
|
631
|
-
**Example response:**
|
|
632
|
-
|
|
633
|
-
```
|
|
634
|
-
Found 2 packages by alice:
|
|
635
|
-
|
|
636
|
-
- alice.ui-kit [pool] — A modern UI component kit for React (5,400 downloads)
|
|
637
|
-
- alice.form-validator [marketplace] — Form validation utilities (1,200 downloads)
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### lpm_pool_stats
|
|
641
|
-
|
|
642
|
-
Get your Pool revenue sharing earnings estimate for the current month. Shows per-package breakdown with installs, weighted downloads, share percentage, and estimated earnings.
|
|
643
|
-
|
|
644
|
-
**Parameters:** None
|
|
645
|
-
|
|
646
|
-
**Example response:**
|
|
647
|
-
|
|
648
|
-
```json
|
|
649
|
-
{
|
|
650
|
-
"billingPeriod": "2026-02",
|
|
651
|
-
"totalWeightedDownloads": 5000,
|
|
652
|
-
"estimatedEarningsCents": 2450,
|
|
653
|
-
"packages": [
|
|
654
|
-
{
|
|
655
|
-
"name": "@lpm.dev/alice.my-utils",
|
|
656
|
-
"installCount": 120,
|
|
657
|
-
"weightedDownloads": 3200,
|
|
658
|
-
"sharePercentage": 1.85,
|
|
659
|
-
"estimatedEarningsCents": 1800
|
|
660
|
-
}
|
|
661
|
-
]
|
|
662
|
-
}
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### lpm_user_info
|
|
666
|
-
|
|
667
|
-
Get information about the authenticated LPM user including organizations, plan tier, pool access, and usage limits.
|
|
668
|
-
|
|
669
|
-
**Parameters:** None
|
|
670
|
-
|
|
671
|
-
**Example response:**
|
|
672
|
-
|
|
673
|
-
```json
|
|
674
|
-
{
|
|
675
|
-
"username": "alice@example.com",
|
|
676
|
-
"profile_username": "alice",
|
|
677
|
-
"organizations": [{ "slug": "acme", "name": "Acme Corp", "role": "owner" }],
|
|
678
|
-
"available_scopes": ["@alice", "@acme"],
|
|
679
|
-
"plan_tier": "pro",
|
|
680
|
-
"has_pool_access": true,
|
|
681
|
-
"usage": { "storage_bytes": 50000000, "private_packages": 2 },
|
|
682
|
-
"limits": { "privatePackages": 10, "storageBytes": 524288000 }
|
|
683
|
-
}
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
## Caching
|
|
687
|
-
|
|
688
|
-
Responses are cached in memory to reduce API calls:
|
|
689
|
-
|
|
690
|
-
| Tool | Cache TTL |
|
|
691
|
-
| ------------------------- | --------- |
|
|
692
|
-
| `lpm_search` | 5 minutes |
|
|
693
|
-
| `lpm_package_info` | 5 minutes |
|
|
694
|
-
| `lpm_api_docs` | 5 minutes |
|
|
695
|
-
| `lpm_llm_context` | 5 minutes |
|
|
696
|
-
| `lpm_package_context` | 5 minutes |
|
|
697
|
-
| `lpm_package_skills` | 5 minutes |
|
|
698
|
-
| `lpm_browse_source` | 5 minutes |
|
|
699
|
-
| `lpm_marketplace_info` | 5 minutes |
|
|
700
|
-
| `lpm_docs` | 30 minutes |
|
|
701
|
-
| `lpm_quality_report` | 5 minutes |
|
|
702
|
-
| `lpm_search_owners` | 5 minutes |
|
|
703
|
-
| `lpm_packages_by_owner` | 5 minutes |
|
|
704
|
-
| `lpm_pool_stats` | 1 hour |
|
|
705
|
-
| `lpm_user_info` | 5 minutes |
|
|
706
|
-
| `lpm_audit` | No cache |
|
|
707
|
-
| `lpm_add` | No cache |
|
|
708
|
-
| `lpm_install` | No cache |
|
|
709
|
-
|
|
710
|
-
Cache is in-memory only and resets when the server restarts.
|
|
711
|
-
|
|
712
123
|
## Troubleshooting
|
|
713
124
|
|
|
714
125
|
**"No LPM token found"**
|
package/lib/auth.js
CHANGED
|
@@ -1,36 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { isIP } from "node:net"
|
|
1
3
|
import {
|
|
2
4
|
DEFAULT_REGISTRY_URL,
|
|
3
|
-
|
|
5
|
+
KEYTAR_ACCOUNT_PREFIX,
|
|
4
6
|
KEYTAR_SERVICE_NAME,
|
|
5
7
|
} from "./constants.js"
|
|
6
8
|
|
|
9
|
+
const KEYCHAIN_READ_ERROR =
|
|
10
|
+
"Unable to read LPM credentials from the OS keychain. Set LPM_TOKEN or run `lpm login` and retry."
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Resolve the LPM auth token.
|
|
9
14
|
* Priority: LPM_TOKEN env var → OS keychain (via keytar) → null
|
|
10
15
|
*
|
|
16
|
+
* @param {() => Promise<unknown>} [loadKeytar]
|
|
11
17
|
* @returns {Promise<string | null>}
|
|
12
18
|
*/
|
|
13
|
-
export async function getToken() {
|
|
14
|
-
// 1. Environment variable (recommended for MCP configs)
|
|
19
|
+
export async function getToken(loadKeytar = () => import("keytar")) {
|
|
15
20
|
if (process.env.LPM_TOKEN) {
|
|
16
21
|
return process.env.LPM_TOKEN
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
let keytarModule
|
|
20
25
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
keytarModule = await loadKeytar()
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (
|
|
29
|
+
error &&
|
|
30
|
+
typeof error === "object" &&
|
|
31
|
+
"code" in error &&
|
|
32
|
+
(error.code === "ERR_MODULE_NOT_FOUND" ||
|
|
33
|
+
error.code === "MODULE_NOT_FOUND")
|
|
34
|
+
) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
throw new Error(KEYCHAIN_READ_ERROR)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const keytar = keytarModule.default ?? keytarModule
|
|
42
|
+
for (const account of keychainAccountsToTry(getBaseUrl())) {
|
|
43
|
+
const token = await keytar.getPassword(KEYTAR_SERVICE_NAME, account)
|
|
44
|
+
if (token) return token
|
|
45
|
+
}
|
|
27
46
|
} catch {
|
|
28
|
-
|
|
47
|
+
throw new Error(KEYCHAIN_READ_ERROR)
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
return null
|
|
32
51
|
}
|
|
33
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The accounts that may hold the login for `registryUrl`, whichever way it
|
|
55
|
+
* ends: the one LPM CLI uses now, then the one CLI releases before 0.86 used
|
|
56
|
+
* for a login made with the registry spelled with a trailing slash.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} registryUrl
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
function keychainAccountsToTry(registryUrl) {
|
|
62
|
+
const canonical = registryUrl.replace(/\/+$/, "")
|
|
63
|
+
return [accountForKey(canonical), accountForKey(`${canonical}/`)]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply the LPM CLI's default registry transport contract: HTTPS, or HTTP
|
|
68
|
+
* only for loopback. The MCP server has no `--insecure` remote-HTTP escape hatch.
|
|
69
|
+
*
|
|
70
|
+
* @param {string | undefined} raw
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function resolveRegistryUrl(raw = process.env.LPM_REGISTRY_URL) {
|
|
74
|
+
if (!raw) return DEFAULT_REGISTRY_URL
|
|
75
|
+
|
|
76
|
+
let parsed
|
|
77
|
+
try {
|
|
78
|
+
parsed = new URL(raw)
|
|
79
|
+
} catch {
|
|
80
|
+
return DEFAULT_REGISTRY_URL
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (parsed.protocol === "https:") return raw
|
|
84
|
+
if (parsed.protocol === "http:" && isLoopbackHost(parsed.hostname)) return raw
|
|
85
|
+
return DEFAULT_REGISTRY_URL
|
|
86
|
+
}
|
|
87
|
+
|
|
34
88
|
/**
|
|
35
89
|
* Resolve the LPM registry base URL.
|
|
36
90
|
* Priority: LPM_REGISTRY_URL env var → default
|
|
@@ -38,5 +92,43 @@ export async function getToken() {
|
|
|
38
92
|
* @returns {string}
|
|
39
93
|
*/
|
|
40
94
|
export function getBaseUrl() {
|
|
41
|
-
return
|
|
95
|
+
return resolveRegistryUrl()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Derive the keychain account used by LPM CLI for one effective registry URL.
|
|
100
|
+
* LPM CLI keys logins by the registry without trailing slashes, so both
|
|
101
|
+
* spellings name one account.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} [registryUrl]
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function getKeychainAccount(registryUrl = getBaseUrl()) {
|
|
107
|
+
return accountForKey(registryUrl.replace(/\/+$/, ""))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {string} key */
|
|
111
|
+
function accountForKey(key) {
|
|
112
|
+
const digest = createHash("sha256").update(key, "utf8").digest("hex")
|
|
113
|
+
return `${KEYTAR_ACCOUNT_PREFIX}:${digest.slice(0, 16)}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLoopbackHost(host) {
|
|
117
|
+
const unbracketed =
|
|
118
|
+
host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host
|
|
119
|
+
if (unbracketed.toLowerCase() === "localhost") return true
|
|
120
|
+
|
|
121
|
+
const version = isIP(unbracketed)
|
|
122
|
+
if (version === 4) return unbracketed.split(".", 1)[0] === "127"
|
|
123
|
+
if (version !== 6) return false
|
|
124
|
+
|
|
125
|
+
const normalized = new URL(`http://[${unbracketed}]/`).hostname
|
|
126
|
+
.slice(1, -1)
|
|
127
|
+
.toLowerCase()
|
|
128
|
+
if (normalized === "::1") return true
|
|
129
|
+
|
|
130
|
+
const mapped = normalized.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
|
|
131
|
+
if (!mapped) return false
|
|
132
|
+
const high = Number.parseInt(mapped[1], 16)
|
|
133
|
+
return high >> 8 === 127
|
|
42
134
|
}
|
package/lib/cli.js
CHANGED
|
@@ -1,41 +1,83 @@
|
|
|
1
|
-
import { execFile
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs"
|
|
3
|
+
import { createRequire } from "node:module"
|
|
4
|
+
import path from "node:path"
|
|
2
5
|
|
|
3
6
|
const CLI_TIMEOUT_MS = 60_000
|
|
4
7
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* Resolution order:
|
|
9
|
-
* 1. LPM_CLI_PATH env var (explicit override)
|
|
10
|
-
* 2. `which lpm` (global install)
|
|
11
|
-
* 3. null (caller should handle fallback)
|
|
12
|
-
*
|
|
13
|
-
* @returns {string|null}
|
|
14
|
-
*/
|
|
15
|
-
export function resolveCli() {
|
|
16
|
-
// 1. Explicit override
|
|
8
|
+
/** Resolve an explicit CLI path or a global installation outside the caller's project. */
|
|
9
|
+
export function resolveCli(platform = process.platform) {
|
|
17
10
|
const envPath = process.env.LPM_CLI_PATH
|
|
18
|
-
if (envPath)
|
|
11
|
+
if (envPath) {
|
|
12
|
+
const syntax = platform === "win32" ? path.win32 : path.posix
|
|
13
|
+
const qualified =
|
|
14
|
+
syntax.isAbsolute(envPath) &&
|
|
15
|
+
(platform !== "win32" || syntax.parse(envPath).root.length > 1)
|
|
16
|
+
return qualified ? envPath : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const workspace = realpathSync(process.cwd())
|
|
20
|
+
for (const directory of (process.env.PATH || "").split(path.delimiter)) {
|
|
21
|
+
if (!path.isAbsolute(directory)) continue
|
|
22
|
+
const executable = globalExecutable(
|
|
23
|
+
path.join(directory, platform === "win32" ? "lpm.exe" : "lpm"),
|
|
24
|
+
workspace,
|
|
25
|
+
platform,
|
|
26
|
+
)
|
|
27
|
+
if (executable) return executable
|
|
28
|
+
if (platform !== "win32") continue
|
|
29
|
+
|
|
30
|
+
// npm exposes a .cmd shim. Resolve its native dependency without a shell.
|
|
31
|
+
try {
|
|
32
|
+
const wrapper = path.join(
|
|
33
|
+
directory,
|
|
34
|
+
"node_modules",
|
|
35
|
+
"@lpm-registry",
|
|
36
|
+
"cli",
|
|
37
|
+
"package.json",
|
|
38
|
+
)
|
|
39
|
+
if (!globalExecutable(wrapper, workspace, platform)) continue
|
|
40
|
+
const require = createRequire(wrapper)
|
|
41
|
+
const nativePackage = require.resolve(
|
|
42
|
+
"@lpm-registry/cli-win32-x64/package.json",
|
|
43
|
+
)
|
|
44
|
+
const native = globalExecutable(
|
|
45
|
+
path.join(path.dirname(nativePackage), "lpm.exe"),
|
|
46
|
+
workspace,
|
|
47
|
+
platform,
|
|
48
|
+
)
|
|
49
|
+
if (native) return native
|
|
50
|
+
} catch {
|
|
51
|
+
// Continue to the next PATH directory if optional native dependencies are absent.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
19
56
|
|
|
20
|
-
|
|
57
|
+
function globalExecutable(candidate, workspace, platform) {
|
|
21
58
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
const resolved = realpathSync(candidate)
|
|
60
|
+
const relative = path.relative(workspace, resolved)
|
|
61
|
+
if (
|
|
62
|
+
relative === "" ||
|
|
63
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
64
|
+
relative !== ".." &&
|
|
65
|
+
!path.isAbsolute(relative))
|
|
66
|
+
)
|
|
67
|
+
return null
|
|
68
|
+
if (!statSync(resolved).isFile()) return null
|
|
69
|
+
accessSync(resolved, platform === "win32" ? constants.F_OK : constants.X_OK)
|
|
70
|
+
return resolved
|
|
27
71
|
} catch {
|
|
28
|
-
|
|
72
|
+
return null
|
|
29
73
|
}
|
|
30
|
-
|
|
31
|
-
return null
|
|
32
74
|
}
|
|
33
75
|
|
|
34
76
|
/**
|
|
35
77
|
* Run an LPM CLI command and return parsed JSON output.
|
|
36
78
|
*
|
|
37
79
|
* @param {string[]} args - CLI arguments (e.g., ['add', '@lpm.dev/owner.pkg', '--json'])
|
|
38
|
-
* @param {{ timeout?: number }} options
|
|
80
|
+
* @param {{ timeout?: number, cwd?: string }} options
|
|
39
81
|
* @returns {Promise<{ success: boolean, data: object|null, error: string|null }>}
|
|
40
82
|
*/
|
|
41
83
|
export async function runCli(args, options = {}) {
|
|
@@ -58,6 +100,7 @@ export async function runCli(args, options = {}) {
|
|
|
58
100
|
args,
|
|
59
101
|
{
|
|
60
102
|
timeout,
|
|
103
|
+
cwd: options.cwd,
|
|
61
104
|
maxBuffer: 10 * 1024 * 1024,
|
|
62
105
|
env: { ...process.env },
|
|
63
106
|
},
|
|
@@ -74,13 +117,23 @@ export async function runCli(args, options = {}) {
|
|
|
74
117
|
// Try to parse JSON from stdout
|
|
75
118
|
try {
|
|
76
119
|
const data = JSON.parse(stdout)
|
|
120
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
121
|
+
throw new Error("CLI returned an invalid result")
|
|
122
|
+
}
|
|
123
|
+
const success = !err && data.success !== false
|
|
124
|
+
const reason =
|
|
125
|
+
typeof data.error === "string" ? data.error : data.error?.message
|
|
77
126
|
resolve({
|
|
78
|
-
success
|
|
127
|
+
success,
|
|
79
128
|
data,
|
|
80
|
-
error:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
129
|
+
error: success
|
|
130
|
+
? null
|
|
131
|
+
: reason ||
|
|
132
|
+
data.errors?.[0] ||
|
|
133
|
+
(err?.killed
|
|
134
|
+
? `CLI command timed out after ${timeout / 1000}s`
|
|
135
|
+
: err?.message) ||
|
|
136
|
+
"Command failed",
|
|
84
137
|
})
|
|
85
138
|
} catch {
|
|
86
139
|
// CLI produced output but not valid JSON
|
package/lib/constants.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export const DEFAULT_REGISTRY_URL = "https://lpm.dev"
|
|
4
4
|
|
|
5
5
|
export const KEYTAR_SERVICE_NAME = "lpm-cli"
|
|
6
|
-
export const
|
|
6
|
+
export const KEYTAR_ACCOUNT_PREFIX = "auth-token"
|
|
7
7
|
|
|
8
8
|
export const REQUEST_TIMEOUT_MS = 30_000
|
|
9
9
|
export const MAX_RETRIES = 2
|
|
@@ -31,5 +31,5 @@ export const ERROR_MESSAGES = {
|
|
|
31
31
|
timeout: "Request timed out. Try again later.",
|
|
32
32
|
invalidName:
|
|
33
33
|
"Invalid package name format. Expected: owner.package-name or @lpm.dev/owner.package-name",
|
|
34
|
-
searchNoParams:
|
|
34
|
+
searchNoParams: "A search query or structured filter is required.",
|
|
35
35
|
}
|
package/lib/format.js
CHANGED
|
@@ -65,3 +65,11 @@ export function parseName(input) {
|
|
|
65
65
|
|
|
66
66
|
return { owner, name }
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/** Preserve CLI reports and mark failed process results as MCP tool errors. */
|
|
70
|
+
export function cliResponse(result) {
|
|
71
|
+
if (!result.data) return errorResponse(result.error || "Command failed")
|
|
72
|
+
const response = jsonResponse(result.data)
|
|
73
|
+
if (!result.success) response.isError = true
|
|
74
|
+
return response
|
|
75
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
2
3
|
import { z } from "zod"
|
|
3
4
|
import { getBaseUrl, getToken } from "./auth.js"
|
|
@@ -20,6 +21,8 @@ import { search } from "./tools/search.js"
|
|
|
20
21
|
import { searchOwners } from "./tools/search-owners.js"
|
|
21
22
|
import { userInfo } from "./tools/user-info.js"
|
|
22
23
|
|
|
24
|
+
const { version } = createRequire(import.meta.url)("../package.json")
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* Create and configure the MCP server with all LPM tools.
|
|
25
28
|
* @returns {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer}
|
|
@@ -27,7 +30,7 @@ import { userInfo } from "./tools/user-info.js"
|
|
|
27
30
|
export function createServer() {
|
|
28
31
|
const server = new McpServer({
|
|
29
32
|
name: "lpm-registry",
|
|
30
|
-
version
|
|
33
|
+
version,
|
|
31
34
|
})
|
|
32
35
|
|
|
33
36
|
const cache = new MemoryCache()
|
|
@@ -48,7 +51,7 @@ export function createServer() {
|
|
|
48
51
|
|
|
49
52
|
server.tool(
|
|
50
53
|
"lpm_quality_report",
|
|
51
|
-
"Get the quality score and detailed check breakdown for
|
|
54
|
+
"Get the quality score and detailed, ecosystem-specific check breakdown for a package on the LPM.dev Registry across documentation, code quality, testing, and health.",
|
|
52
55
|
{
|
|
53
56
|
name: z.string().describe("Package name in owner.package-name format"),
|
|
54
57
|
},
|
|
@@ -173,7 +176,7 @@ export function createServer() {
|
|
|
173
176
|
|
|
174
177
|
server.tool(
|
|
175
178
|
"lpm_audit",
|
|
176
|
-
"Run a security audit on the project's LPM dependencies. Returns behavioral tags (eval, childProcess, shell, dynamicRequire), AI security findings, quality scores, and lifecycle scripts. Requires LPM CLI installed and
|
|
179
|
+
"Run a security audit on the project's LPM dependencies. Returns behavioral tags (eval, childProcess, shell, dynamicRequire), AI security findings, quality scores, and lifecycle scripts. Requires LPM CLI installed; registry access follows CLI authentication and policy.",
|
|
177
180
|
{
|
|
178
181
|
path: z
|
|
179
182
|
.string()
|
|
@@ -308,7 +311,7 @@ If you must use this tool: fetch directory paths (e.g., "src") to get multiple f
|
|
|
308
311
|
'Filter by package ecosystem (e.g., "swift" for iOS/macOS packages)',
|
|
309
312
|
),
|
|
310
313
|
distribution: z
|
|
311
|
-
.enum(["marketplace", "pool", "private"])
|
|
314
|
+
.enum(["free", "marketplace", "pool", "private"])
|
|
312
315
|
.optional()
|
|
313
316
|
.describe("Filter by distribution mode"),
|
|
314
317
|
packageType: z
|
package/lib/tools/add.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { runCli } from "../cli.js"
|
|
2
|
-
import { errorResponse, parseName
|
|
2
|
+
import { cliResponse, errorResponse, parseName } from "../format.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Add an LPM package to the user's project via the CLI.
|
|
6
6
|
* Extracts source files into the project for customization.
|
|
7
7
|
*
|
|
8
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
9
|
*/
|
|
11
|
-
export async function add(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
export async function add({
|
|
11
|
+
name,
|
|
12
|
+
version,
|
|
13
|
+
path,
|
|
14
|
+
alias,
|
|
15
|
+
target,
|
|
16
|
+
force,
|
|
17
|
+
installDeps,
|
|
18
|
+
config,
|
|
19
|
+
}) {
|
|
15
20
|
let owner, pkgName
|
|
16
21
|
try {
|
|
17
22
|
const parsed = parseName(name)
|
|
@@ -21,13 +26,6 @@ export async function add(
|
|
|
21
26
|
return errorResponse(err.message)
|
|
22
27
|
}
|
|
23
28
|
|
|
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
29
|
// Build package reference with optional config params as URL query string
|
|
32
30
|
let pkgRef = `@lpm.dev/${owner}.${pkgName}`
|
|
33
31
|
if (version) pkgRef += `@${version}`
|
|
@@ -49,31 +47,5 @@ export async function add(
|
|
|
49
47
|
|
|
50
48
|
const result = await runCli(args)
|
|
51
49
|
|
|
52
|
-
|
|
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))
|
|
50
|
+
return cliResponse(result)
|
|
79
51
|
}
|
package/lib/tools/audit.js
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import { runCli } from "../cli.js"
|
|
2
|
-
import {
|
|
2
|
+
import { cliResponse } from "../format.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Run a security audit on the project's LPM dependencies.
|
|
6
6
|
* Returns behavioral tags, AI security findings, quality scores, and lifecycle scripts.
|
|
7
7
|
*
|
|
8
8
|
* @param {{ path?: string }} params
|
|
9
|
-
* @param {{ getToken: () => Promise<string|null> }} ctx
|
|
10
9
|
*/
|
|
11
|
-
export async function audit({ path }
|
|
12
|
-
const token = await ctx.getToken()
|
|
13
|
-
if (!token) {
|
|
14
|
-
return errorResponse(
|
|
15
|
-
"Authentication required. Set LPM_TOKEN environment variable or run `lpm login`.",
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
10
|
+
export async function audit({ path }) {
|
|
19
11
|
const args = ["audit", "--json"]
|
|
20
12
|
|
|
21
13
|
const result = await runCli(args, {
|
|
@@ -23,28 +15,5 @@ export async function audit({ path }, ctx) {
|
|
|
23
15
|
...(path && { cwd: path }),
|
|
24
16
|
})
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
return errorResponse(result.error || "Failed to run audit.")
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const data = result.data
|
|
31
|
-
|
|
32
|
-
// Format for AI consumption
|
|
33
|
-
const summary = {
|
|
34
|
-
success: true,
|
|
35
|
-
totalPackages: data.totalPackages || 0,
|
|
36
|
-
packagesWithIssues: data.packagesWithIssues || 0,
|
|
37
|
-
packages: (data.packages || []).map(pkg => ({
|
|
38
|
-
name: pkg.name,
|
|
39
|
-
version: pkg.version,
|
|
40
|
-
qualityScore: pkg.qualityScore,
|
|
41
|
-
issues: pkg.issues || [],
|
|
42
|
-
})),
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (data.summary) {
|
|
46
|
-
summary.summary = data.summary
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return textResponse(JSON.stringify(summary, null, 2))
|
|
18
|
+
return cliResponse(result)
|
|
50
19
|
}
|
package/lib/tools/install.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { runCli } from "../cli.js"
|
|
2
|
-
import { errorResponse, parseName
|
|
2
|
+
import { cliResponse, errorResponse, parseName } from "../format.js"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Install an LPM package as a dependency via the CLI.
|
|
6
6
|
* Installs to node_modules like npm install.
|
|
7
7
|
*
|
|
8
8
|
* @param {{ name: string, version?: string }} params
|
|
9
|
-
* @param {{ getToken: () => Promise<string|null> }} ctx
|
|
10
9
|
*/
|
|
11
|
-
export async function install({ name, version }
|
|
10
|
+
export async function install({ name, version }) {
|
|
12
11
|
let owner, pkgName
|
|
13
12
|
try {
|
|
14
13
|
const parsed = parseName(name)
|
|
@@ -18,13 +17,6 @@ export async function install({ name, version }, ctx) {
|
|
|
18
17
|
return errorResponse(err.message)
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
const token = await ctx.getToken()
|
|
22
|
-
if (!token) {
|
|
23
|
-
return errorResponse(
|
|
24
|
-
"Authentication required. Set LPM_TOKEN environment variable or run `lpm login`.",
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
20
|
let pkgRef = `@lpm.dev/${owner}.${pkgName}`
|
|
29
21
|
if (version) pkgRef += `@${version}`
|
|
30
22
|
|
|
@@ -32,20 +24,5 @@ export async function install({ name, version }, ctx) {
|
|
|
32
24
|
|
|
33
25
|
const result = await runCli(args)
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
return errorResponse(result.error || "Failed to install package.")
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const data = result.data
|
|
40
|
-
const summary = {
|
|
41
|
-
success: true,
|
|
42
|
-
packages: data.packages || [{ name: pkgRef }],
|
|
43
|
-
npmOutput: data.npmOutput || "",
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (data.warnings?.length > 0) {
|
|
47
|
-
summary.warnings = data.warnings
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return textResponse(JSON.stringify(summary, null, 2))
|
|
27
|
+
return cliResponse(result)
|
|
51
28
|
}
|
package/lib/tools/search.js
CHANGED
|
@@ -10,10 +10,10 @@ import { errorResponse, textResponse } from "../format.js"
|
|
|
10
10
|
* - Explore path (/packages/explore): when any structured filter is active
|
|
11
11
|
*
|
|
12
12
|
* @param {object} params
|
|
13
|
-
* @param {string} [params.query] - Search text (
|
|
13
|
+
* @param {string} [params.query] - Search text (optional with a structured filter)
|
|
14
14
|
* @param {string} [params.category] - Category slug filter
|
|
15
15
|
* @param {string} [params.ecosystem] - Ecosystem filter (js, swift, xcframework)
|
|
16
|
-
* @param {string} [params.distribution] - Distribution mode filter (marketplace, pool, private)
|
|
16
|
+
* @param {string} [params.distribution] - Distribution mode filter (free, marketplace, pool, private)
|
|
17
17
|
* @param {string} [params.packageType] - Package type filter
|
|
18
18
|
* @param {string} [params.sort] - Sort order (newest, popular, name)
|
|
19
19
|
* @param {boolean} [params.hasTypes] - TypeScript types filter
|
|
@@ -36,18 +36,6 @@ export async function search(params, ctx) {
|
|
|
36
36
|
minNodeVersion,
|
|
37
37
|
} = params
|
|
38
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
39
|
const hasStructuredFilters =
|
|
52
40
|
category ||
|
|
53
41
|
distribution ||
|
|
@@ -56,7 +44,18 @@ export async function search(params, ctx) {
|
|
|
56
44
|
hasTypes ||
|
|
57
45
|
moduleType ||
|
|
58
46
|
license ||
|
|
59
|
-
minNodeVersion
|
|
47
|
+
minNodeVersion ||
|
|
48
|
+
(!query?.trim() && params.ecosystem)
|
|
49
|
+
|
|
50
|
+
if (!query?.trim() && !hasStructuredFilters) {
|
|
51
|
+
return errorResponse(ERROR_MESSAGES.searchNoParams)
|
|
52
|
+
}
|
|
53
|
+
if (query && query.trim().length < 2) {
|
|
54
|
+
return errorResponse("Search query must be at least 2 characters.")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const token = await ctx.getToken()
|
|
58
|
+
const baseUrl = ctx.getBaseUrl()
|
|
60
59
|
|
|
61
60
|
// Build deterministic cache key from all params
|
|
62
61
|
const cacheKey = buildCacheKey(params)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lpm-registry/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"author": "LPM <hello@lpm.dev> (https://lpm.dev)",
|
|
6
6
|
"repository": {
|