@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 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
- If your CLI is configured to use a custom registry URL (for example local dev), `lpm mcp setup` automatically writes `LPM_REGISTRY_URL` into MCP config so editor-launched MCP processes hit the same registry.
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
- The server reads your token from the OS keychain (set by `lpm login`). No token in config files required.
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
- Alternatively, set the `LPM_TOKEN` environment variable for environments without keychain access:
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 | Description | Auth Required |
96
- | ------------------------- | --------------------------------------------------------------- | ------------- |
97
- | `lpm_search` | Search packages with natural language or structured filters | Optional |
98
- | `lpm_package_info` | Get package metadata, install method, access model, and readme | Optional |
99
- | `lpm_api_docs` | Get structured API docs — functions, classes, types, signatures | Optional |
100
- | `lpm_llm_context` | Get LLM-optimized usage guide — quickStart, patterns, gotchas | Optional |
101
- | `lpm_package_context` | Get complete package context in one call (pre-install evaluation) | Optional |
102
- | `lpm_package_skills` | Get Agent Skills for building with an installed package | Optional |
103
- | `lpm_docs` | Search or read LPM documentation (setup, CLI, publishing, etc.) | No |
104
- | `lpm_browse_source` | Browse package source code remotely (last resort) | Yes |
105
- | `lpm_add` | Add a package by extracting source files into the project | Yes |
106
- | `lpm_install` | Install a managed dependency (JS → node_modules, Swift → Package.swift) | Yes |
107
- | `lpm_audit` | Security audit — behavioral tags, AI findings, quality scores | Yes |
108
- | `lpm_marketplace_info` | Marketplace pricing, licensing, and seat management | Optional |
109
- | `lpm_quality_report` | Get quality score and 28-check breakdown | Optional |
110
- | `lpm_search_owners` | Search for users or organizations by name | No |
111
- | `lpm_packages_by_owner` | List packages published by a specific user or org | No |
112
- | `lpm_pool_stats` | Get your Pool revenue earnings for the current month | Yes |
113
- | `lpm_user_info` | Get authenticated user info, orgs, and usage | Yes |
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
- KEYTAR_ACCOUNT_NAME,
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
- // 2. OS keychain (reads from same store as `lpm login`)
24
+ let keytarModule
20
25
  try {
21
- const keytar = await import("keytar")
22
- const token = await keytar.default.getPassword(
23
- KEYTAR_SERVICE_NAME,
24
- KEYTAR_ACCOUNT_NAME,
25
- )
26
- if (token) return token
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
- // keytar not available — this is expected in many environments
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 process.env.LPM_REGISTRY_URL || DEFAULT_REGISTRY_URL
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, execSync } from "node:child_process"
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
- * Resolve the path to the `lpm` CLI binary.
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) return 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
- // 2. Global install
57
+ function globalExecutable(candidate, workspace, platform) {
21
58
  try {
22
- const result = execSync("which lpm", {
23
- encoding: "utf-8",
24
- timeout: 5_000,
25
- }).trim()
26
- if (result) return result
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
- // not found
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: data.success !== false,
127
+ success,
79
128
  data,
80
- error:
81
- data.success === false
82
- ? data.errors?.[0] || "Command failed"
83
- : null,
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 KEYTAR_ACCOUNT_NAME = "auth-token"
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: 'At least one of "query" or "category" is required.',
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: "0.2.0",
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 an LPM package (27 checks across documentation, code, testing, health)",
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 authentication.",
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, textResponse } from "../format.js"
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
- { name, version, path, alias, target, force, installDeps, config },
13
- ctx,
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
- 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))
50
+ return cliResponse(result)
79
51
  }
@@ -1,21 +1,13 @@
1
1
  import { runCli } from "../cli.js"
2
- import { errorResponse, textResponse } from "../format.js"
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 }, ctx) {
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
- if (!result.success) {
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
  }
@@ -1,14 +1,13 @@
1
1
  import { runCli } from "../cli.js"
2
- import { errorResponse, parseName, textResponse } from "../format.js"
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 }, ctx) {
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
- if (!result.success) {
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
  }
@@ -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 (required unless category provided)
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.2.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": {