@lpm-registry/mcp-server 0.1.1 → 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 -564
- 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 +52 -17
- package/lib/tools/add.js +12 -40
- package/lib/tools/audit.js +19 -0
- package/lib/tools/docs.js +101 -0
- package/lib/tools/get-install-command.js +11 -11
- package/lib/tools/install.js +3 -26
- package/lib/tools/marketplace-info.js +80 -0
- package/lib/tools/package-info.js +12 -8
- package/lib/tools/search.js +14 -15
- package/package.json +1 -1
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,13 +1,16 @@
|
|
|
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"
|
|
4
5
|
import { MemoryCache } from "./cache.js"
|
|
5
6
|
import { add } from "./tools/add.js"
|
|
6
7
|
import { apiDocs } from "./tools/api-docs.js"
|
|
8
|
+
import { audit } from "./tools/audit.js"
|
|
7
9
|
import { browseSource } from "./tools/browse-source.js"
|
|
8
|
-
import {
|
|
10
|
+
import { docs } from "./tools/docs.js"
|
|
9
11
|
import { install } from "./tools/install.js"
|
|
10
12
|
import { llmContext } from "./tools/llm-context.js"
|
|
13
|
+
import { marketplaceInfo } from "./tools/marketplace-info.js"
|
|
11
14
|
import { packageContext } from "./tools/package-context.js"
|
|
12
15
|
import { packageInfo } from "./tools/package-info.js"
|
|
13
16
|
import { packageSkills } from "./tools/package-skills.js"
|
|
@@ -18,6 +21,8 @@ import { search } from "./tools/search.js"
|
|
|
18
21
|
import { searchOwners } from "./tools/search-owners.js"
|
|
19
22
|
import { userInfo } from "./tools/user-info.js"
|
|
20
23
|
|
|
24
|
+
const { version } = createRequire(import.meta.url)("../package.json")
|
|
25
|
+
|
|
21
26
|
/**
|
|
22
27
|
* Create and configure the MCP server with all LPM tools.
|
|
23
28
|
* @returns {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer}
|
|
@@ -25,7 +30,7 @@ import { userInfo } from "./tools/user-info.js"
|
|
|
25
30
|
export function createServer() {
|
|
26
31
|
const server = new McpServer({
|
|
27
32
|
name: "lpm-registry",
|
|
28
|
-
version
|
|
33
|
+
version,
|
|
29
34
|
})
|
|
30
35
|
|
|
31
36
|
const cache = new MemoryCache()
|
|
@@ -46,7 +51,7 @@ export function createServer() {
|
|
|
46
51
|
|
|
47
52
|
server.tool(
|
|
48
53
|
"lpm_quality_report",
|
|
49
|
-
"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.",
|
|
50
55
|
{
|
|
51
56
|
name: z.string().describe("Package name in owner.package-name format"),
|
|
52
57
|
},
|
|
@@ -89,7 +94,7 @@ export function createServer() {
|
|
|
89
94
|
|
|
90
95
|
server.tool(
|
|
91
96
|
"lpm_package_context",
|
|
92
|
-
"Get complete context for an LPM package in a single call — combines condensed package metadata (name, version, description, install method, dependencies), structured API docs (functions, classes, types), and LLM usage guide (quickStart, patterns, gotchas). Use this
|
|
97
|
+
"Get complete context for an LPM package in a single call — combines condensed package metadata (name, version, description, install method, dependencies), structured API docs (functions, classes, types), and LLM usage guide (quickStart, patterns, gotchas). Use this BEFORE installing to evaluate and understand a package. If the package is already installed locally, prefer reading local files directly and use lpm_package_skills for usage patterns instead.",
|
|
93
98
|
{
|
|
94
99
|
name: z
|
|
95
100
|
.string()
|
|
@@ -106,7 +111,7 @@ export function createServer() {
|
|
|
106
111
|
|
|
107
112
|
server.tool(
|
|
108
113
|
"lpm_package_skills",
|
|
109
|
-
"Get author-written Agent Skills for an LPM package
|
|
114
|
+
"Get author-written Agent Skills for an LPM package — usage patterns, anti-patterns, gotchas, and best practices for code generation. Use this when BUILDING with an already-installed package. Skills are version-specific and automatically resolve from local package.json if no version is specified.",
|
|
110
115
|
{
|
|
111
116
|
name: z
|
|
112
117
|
.string()
|
|
@@ -170,25 +175,33 @@ export function createServer() {
|
|
|
170
175
|
)
|
|
171
176
|
|
|
172
177
|
server.tool(
|
|
173
|
-
"
|
|
174
|
-
"
|
|
178
|
+
"lpm_audit",
|
|
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.",
|
|
180
|
+
{
|
|
181
|
+
path: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe("Project directory to audit (defaults to current directory)"),
|
|
185
|
+
},
|
|
186
|
+
params => audit(params, context),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
server.tool(
|
|
190
|
+
"lpm_marketplace_info",
|
|
191
|
+
"Get marketplace information for an LPM package — pricing, licensing model, seat management, and purchase status. Use this before recommending a paid package to check cost and access.",
|
|
175
192
|
{
|
|
176
193
|
name: z
|
|
177
194
|
.string()
|
|
178
195
|
.describe(
|
|
179
196
|
"Package name in owner.package-name or @lpm.dev/owner.package-name format",
|
|
180
197
|
),
|
|
181
|
-
version: z
|
|
182
|
-
.string()
|
|
183
|
-
.optional()
|
|
184
|
-
.describe("Specific version to install (defaults to latest)"),
|
|
185
198
|
},
|
|
186
|
-
params =>
|
|
199
|
+
params => marketplaceInfo(params, context),
|
|
187
200
|
)
|
|
188
201
|
|
|
189
202
|
server.tool(
|
|
190
203
|
"lpm_add",
|
|
191
|
-
"Add an LPM package to the project by extracting source files for customization. Use for UI components, blocks, templates,
|
|
204
|
+
"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.",
|
|
192
205
|
{
|
|
193
206
|
name: z
|
|
194
207
|
.string()
|
|
@@ -235,7 +248,7 @@ export function createServer() {
|
|
|
235
248
|
|
|
236
249
|
server.tool(
|
|
237
250
|
"lpm_install",
|
|
238
|
-
"Install an LPM package as a dependency to node_modules (like npm install). Use for
|
|
251
|
+
"Install an LPM package as a managed dependency. JS packages go to node_modules (like npm install), Swift packages edit Package.swift via SE-0292. Use for libraries, utilities, and SDKs. Requires LPM CLI installed.",
|
|
239
252
|
{
|
|
240
253
|
name: z
|
|
241
254
|
.string()
|
|
@@ -252,9 +265,11 @@ export function createServer() {
|
|
|
252
265
|
|
|
253
266
|
server.tool(
|
|
254
267
|
"lpm_browse_source",
|
|
255
|
-
`Browse source code of an LPM package
|
|
268
|
+
`LAST RESORT: Browse source code of an LPM package remotely. Only use this when you cannot install the package (e.g., evaluating before purchase, checking capabilities without access). If you can install the package, prefer lpm_add or lpm_install first, then read the local files directly — it is faster.
|
|
269
|
+
|
|
270
|
+
Requires authentication. Pool packages require Pool subscription, marketplace packages require a license.
|
|
256
271
|
|
|
257
|
-
|
|
272
|
+
If you must use this tool: fetch directory paths (e.g., "src") to get multiple files in one request rather than individual files.`,
|
|
258
273
|
{
|
|
259
274
|
name: z
|
|
260
275
|
.string()
|
|
@@ -296,7 +311,7 @@ IMPORTANT: Prefer fetching directory paths (e.g., "src", "components") to get al
|
|
|
296
311
|
'Filter by package ecosystem (e.g., "swift" for iOS/macOS packages)',
|
|
297
312
|
),
|
|
298
313
|
distribution: z
|
|
299
|
-
.enum(["marketplace", "pool", "private"])
|
|
314
|
+
.enum(["free", "marketplace", "pool", "private"])
|
|
300
315
|
.optional()
|
|
301
316
|
.describe("Filter by distribution mode"),
|
|
302
317
|
packageType: z
|
|
@@ -353,5 +368,25 @@ IMPORTANT: Prefer fetching directory paths (e.g., "src", "components") to get al
|
|
|
353
368
|
params => search(params, context),
|
|
354
369
|
)
|
|
355
370
|
|
|
371
|
+
server.tool(
|
|
372
|
+
"lpm_docs",
|
|
373
|
+
"Search or read LPM documentation. Use this when the user asks how to use LPM itself — setup, CLI commands, publishing, CI/CD, Swift registry, organizations, billing, etc. Without parameters: returns the docs index. With `page`: returns that specific page. With `query`: searches page titles and returns matching content.",
|
|
374
|
+
{
|
|
375
|
+
page: z
|
|
376
|
+
.string()
|
|
377
|
+
.optional()
|
|
378
|
+
.describe(
|
|
379
|
+
'Documentation page slug (e.g., "cli/commands", "packages/swift-registry", "getting-started/installation")',
|
|
380
|
+
),
|
|
381
|
+
query: z
|
|
382
|
+
.string()
|
|
383
|
+
.optional()
|
|
384
|
+
.describe(
|
|
385
|
+
'Search query to find relevant docs (e.g., "npmrc", "swift", "CI deployment")',
|
|
386
|
+
),
|
|
387
|
+
},
|
|
388
|
+
params => docs(params, context),
|
|
389
|
+
)
|
|
390
|
+
|
|
356
391
|
return server
|
|
357
392
|
}
|
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
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runCli } from "../cli.js"
|
|
2
|
+
import { cliResponse } from "../format.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run a security audit on the project's LPM dependencies.
|
|
6
|
+
* Returns behavioral tags, AI security findings, quality scores, and lifecycle scripts.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ path?: string }} params
|
|
9
|
+
*/
|
|
10
|
+
export async function audit({ path }) {
|
|
11
|
+
const args = ["audit", "--json"]
|
|
12
|
+
|
|
13
|
+
const result = await runCli(args, {
|
|
14
|
+
timeout: 30_000,
|
|
15
|
+
...(path && { cwd: path }),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return cliResponse(result)
|
|
19
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CACHE_TTL } from "../constants.js"
|
|
2
|
+
import { errorResponse, textResponse } from "../format.js"
|
|
3
|
+
|
|
4
|
+
const DOCS_BASE_URL = "https://lpm.dev/api/docs"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Search or read LPM documentation.
|
|
8
|
+
*
|
|
9
|
+
* - Without `page`: returns the full docs index (page titles and slugs).
|
|
10
|
+
* - With `page`: returns the content of that specific documentation page.
|
|
11
|
+
* - With `query`: searches page titles for matches and returns their content.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ page?: string, query?: string }} params
|
|
14
|
+
* @param {{ cache: import('../cache.js').MemoryCache }} ctx
|
|
15
|
+
*/
|
|
16
|
+
export async function docs({ page, query }, ctx) {
|
|
17
|
+
// Mode 1: Fetch a specific page
|
|
18
|
+
if (page) {
|
|
19
|
+
const slug = page.replace(/^\//, "").replace(/\/$/, "")
|
|
20
|
+
const cacheKey = `docs:page:${slug}`
|
|
21
|
+
|
|
22
|
+
const cached = ctx.cache.get(cacheKey)
|
|
23
|
+
if (cached) return cached
|
|
24
|
+
|
|
25
|
+
const response = await fetchText(`${DOCS_BASE_URL}/${slug}`)
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
if (response.status === 404) {
|
|
28
|
+
return errorResponse(
|
|
29
|
+
`Documentation page "${slug}" not found. Use this tool without a page parameter to see all available pages.`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
return errorResponse(`Failed to fetch docs (${response.status})`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = await response.text()
|
|
36
|
+
const result = textResponse(content)
|
|
37
|
+
ctx.cache.set(cacheKey, result, CACHE_TTL.LONG)
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Mode 2: Search or list index
|
|
42
|
+
const cacheKey = "docs:index"
|
|
43
|
+
let indexText = ctx.cache.get(cacheKey)
|
|
44
|
+
|
|
45
|
+
if (!indexText) {
|
|
46
|
+
const response = await fetchText(DOCS_BASE_URL)
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
return errorResponse(`Failed to fetch docs index (${response.status})`)
|
|
49
|
+
}
|
|
50
|
+
indexText = await response.text()
|
|
51
|
+
ctx.cache.set(cacheKey, indexText, CACHE_TTL.LONG)
|
|
52
|
+
} else {
|
|
53
|
+
// Cache stores the textResponse wrapper, extract the text
|
|
54
|
+
indexText =
|
|
55
|
+
typeof indexText === "string"
|
|
56
|
+
? indexText
|
|
57
|
+
: indexText?.content?.[0]?.text || indexText
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If query is provided, filter the index and fetch matching pages
|
|
61
|
+
if (query) {
|
|
62
|
+
const queryLower = query.toLowerCase()
|
|
63
|
+
const lines =
|
|
64
|
+
typeof indexText === "string"
|
|
65
|
+
? indexText.split("\n")
|
|
66
|
+
: String(indexText).split("\n")
|
|
67
|
+
const matches = lines.filter(
|
|
68
|
+
line => line.includes(" — ") && line.toLowerCase().includes(queryLower),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (matches.length === 0) {
|
|
72
|
+
return textResponse(
|
|
73
|
+
`No documentation pages found matching "${query}".\n\nAvailable pages:\n${indexText}`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fetch content of matching pages (up to 3)
|
|
78
|
+
const results = []
|
|
79
|
+
for (const match of matches.slice(0, 3)) {
|
|
80
|
+
const slug = match.split(" — ")[0].trim()
|
|
81
|
+
const response = await fetchText(`${DOCS_BASE_URL}/${slug}`)
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
const content = await response.text()
|
|
84
|
+
results.push(`--- ${slug} ---\n${content}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return textResponse(results.join("\n\n"))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// No query, no page — return the full index
|
|
92
|
+
return textResponse(
|
|
93
|
+
typeof indexText === "string" ? indexText : String(indexText),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function fetchText(url) {
|
|
98
|
+
return fetch(url, {
|
|
99
|
+
headers: { Accept: "text/plain" },
|
|
100
|
+
})
|
|
101
|
+
}
|