From 6362e92b983b9343ee174faa7bef08849a474d2f Mon Sep 17 00:00:00 2001 From: groupguanfang <1203970284@qq.com> Date: Wed, 3 Jun 2026 07:41:18 +0800 Subject: [PATCH 1/6] feat: update --- scripts/upload-release-assets.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/upload-release-assets.ts b/scripts/upload-release-assets.ts index 9862376..816876d 100644 --- a/scripts/upload-release-assets.ts +++ b/scripts/upload-release-assets.ts @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process' -import { mkdir, readdir } from 'node:fs/promises' -import { join, resolve } from 'node:path' +import fs from 'node:fs/promises' +import path from 'node:path' import process from 'node:process' type RunOptions = Parameters[2] @@ -69,28 +69,28 @@ async function resolveExistingReleaseTag(packageTag: string, versionTag: string) } async function main() { - const artifactsDir = resolve(process.env.ARTIFACTS_DIR ?? 'artifacts') - const releaseAssetsDir = resolve(process.env.RELEASE_ASSETS_DIR ?? 'release-assets') + const artifactsDir = path.resolve(process.env.ARTIFACTS_DIR ?? 'artifacts') + const releaseAssetsDir = path.resolve(process.env.RELEASE_ASSETS_DIR ?? 'release-assets') const { version, packageTag, versionTag } = resolveReleaseMeta() - await mkdir(releaseAssetsDir, { recursive: true }) + await fs.mkdir(releaseAssetsDir, { recursive: true }) - const entries = await readdir(artifactsDir, { withFileTypes: true }) + const entries = await fs.readdir(artifactsDir, { withFileTypes: true }) const artifactDirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name) if (artifactDirs.length === 0) { throw new Error(`No artifact directories found in ${artifactsDir}`) } for (const artifactName of artifactDirs) { - const artifactPath = join(artifactsDir, artifactName) - const zipPath = join(releaseAssetsDir, `${artifactName}-${version}.zip`) + const artifactPath = path.join(artifactsDir, artifactName) + const zipPath = path.join(releaseAssetsDir, `${artifactName}-${version}.zip`) await run('zip', ['-r', zipPath, '.'], { cwd: artifactPath }) } const releaseTag = await resolveExistingReleaseTag(packageTag, versionTag) - const zippedAssets = (await readdir(releaseAssetsDir)) + const zippedAssets = (await fs.readdir(releaseAssetsDir)) .filter(file => file.endsWith('.zip')) - .map(file => join(releaseAssetsDir, file)) + .map(file => path.join(releaseAssetsDir, file)) if (zippedAssets.length === 0) { throw new Error(`No zip assets generated in ${releaseAssetsDir}`) } From e6993c0ba0bedb581470550e713263f94b0377ce Mon Sep 17 00:00:00 2001 From: groupguanfang <1203970284@qq.com> Date: Wed, 3 Jun 2026 07:47:47 +0800 Subject: [PATCH 2/6] feat: update --- scripts/upload-release-assets.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/scripts/upload-release-assets.ts b/scripts/upload-release-assets.ts index 816876d..b050736 100644 --- a/scripts/upload-release-assets.ts +++ b/scripts/upload-release-assets.ts @@ -1,34 +1,26 @@ -import { spawn } from 'node:child_process' +import child_process from 'node:child_process' import fs from 'node:fs/promises' import path from 'node:path' import process from 'node:process' -type RunOptions = Parameters[2] - interface PublishedPackage { name: string version: string } -function run(command: string, args: string[], options: RunOptions = {}) { - return new Promise((resolvePromise, rejectPromise) => { - const child = spawn(command, args, { stdio: 'inherit', ...options }) - child.on('error', rejectPromise) - child.on('close', (code) => { - if (code === 0) { - resolvePromise() - return - } - rejectPromise(new Error(`Command failed: ${command} ${args.join(' ')}`)) - }) +function run(command: string, args: string[], options: child_process.SpawnOptionsWithoutStdio = {}) { + return new Promise((resolve, reject) => { + child_process.spawn(command, args, { stdio: 'inherit', ...options }) + .on('error', reject) + .on('close', code => code === 0 ? resolve() : reject(new Error(`Command failed: ${command} ${args.join(' ')}`))) }) } -function runJson(command: string, args: string[], options: RunOptions = {}) { +function runJson(command: string, args: string[], options: child_process.SpawnOptionsWithoutStdio = {}) { return new Promise((resolvePromise, rejectPromise) => { let stdout = '' let stderr = '' - const child = spawn(command, args, { ...options }) + const child = child_process.spawn(command, args, { ...options }) child.stdout?.on('data', chunk => (stdout += String(chunk))) child.stderr?.on('data', chunk => (stderr += String(chunk))) child.on('error', rejectPromise) From d0a7235452a75f9186acbcefaff447b57866dc09 Mon Sep 17 00:00:00 2001 From: groupguanfang <1203970284@qq.com> Date: Wed, 3 Jun 2026 07:55:13 +0800 Subject: [PATCH 3/6] feat: update --- .vscode/settings.json | 1 + install.ps1 | 189 +++++++++++++++++++++++++++++++++++ install.sh | 227 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 install.ps1 mode change 100644 => 100755 install.sh diff --git a/.vscode/settings.json b/.vscode/settings.json index 59a27d0..e1d624b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -56,6 +56,7 @@ "modelcontextprotocol", "Naily", "nodejieba", + "ohosvscode", "vstils" ] } diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..4298dc9 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,189 @@ +#Requires -Version 5.1 +[CmdletBinding()] +param( + [string] $Dir = (Get-Location).Path, + [string] $Version = '', + [switch] $NoGlobal +) + +$ErrorActionPreference = 'Stop' + +$Repo = 'ohosvscode/mcp' +$BinaryName = 'arkts-mcp.exe' +$GithubApi = "https://api.github.com/repos/$Repo" +$Headers = @{ + 'Accept' = 'application/vnd.github+json' + 'User-Agent' = 'arkts-mcp-installer' +} + +function Write-Usage { + @" +Usage: .\install.ps1 [options] + +Download the arkts-mcp executable for this platform, extract it here, and register it globally. + +Options: + -Dir DIR Install directory (default: current directory) + -Version TAG Release tag or version (default: latest release) + -NoGlobal Do not add the install directory to the user PATH + +Examples: + irm https://raw.githubusercontent.com/$Repo/main/install.ps1 | iex + .\install.ps1 -Version 0.0.1-alpha.2 +"@ +} + +function Get-PlatformArtifact { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + switch ($arch) { + 'Arm64' { return 'win-arm64' } + 'X64' { return 'win-x64' } + default { throw "Unsupported Windows architecture: $arch" } + } +} + +function Get-ReleaseJson { + param([string] $VersionTag) + + if ([string]::IsNullOrWhiteSpace($VersionTag)) { + return Invoke-RestMethod -Uri "$GithubApi/releases/latest" -Headers $Headers + } + + $normalized = $VersionTag.TrimStart('v') + $normalized = $normalized -replace '^@arkts/mcp@', '' + $candidates = @( + $VersionTag, + "v$normalized", + "@arkts/mcp@$normalized" + ) + + foreach ($candidate in $candidates) { + try { + return Invoke-RestMethod -Uri "$GithubApi/releases/tags/$candidate" -Headers $Headers + } + catch { + continue + } + } + + throw "Release not found for version: $VersionTag" +} + +function Get-ReleaseAsset { + param( + [object] $Release, + [string] $Platform + ) + + $asset = $Release.assets | Where-Object { $_.name -match "^$([regex]::Escape($Platform))-.*\.zip$" } | Select-Object -First 1 + if (-not $asset) { + throw "No release asset found for platform: $Platform" + } + return $asset +} + +function Save-FileWithProgress { + param( + [string] $Url, + [string] $Destination + ) + + Write-Host 'Downloading...' + + if (Get-Command curl.exe -ErrorAction SilentlyContinue) { + & curl.exe -fL --progress-bar -o $Destination $Url + if ($LASTEXITCODE -ne 0) { + throw "Download failed: $Url" + } + return + } + + $request = [System.Net.HttpWebRequest]::Create($Url) + $request.UserAgent = 'arkts-mcp-installer' + $request.AllowAutoRedirect = $true + $response = $request.GetResponse() + $total = $response.ContentLength + $stream = $response.GetResponseStream() + $fileStream = [System.IO.File]::Open($Destination, [System.IO.FileMode]::Create) + $buffer = New-Object byte[] 8192 + $read = 0 + $downloaded = 0L + + while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) { + $fileStream.Write($buffer, 0, $read) + $downloaded += $read + if ($total -gt 0) { + $percent = [math]::Floor(($downloaded * 100) / $total) + Write-Progress -Activity 'Downloading arkts-mcp' -Status "$downloaded / $total bytes" -PercentComplete $percent + } + } + + Write-Progress -Activity 'Downloading arkts-mcp' -Completed + $fileStream.Close() + $stream.Close() + $response.Close() +} + +function Install-GlobalCommand { + param([string] $InstallDirectory) + + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + $normalizedInstall = $InstallDirectory.TrimEnd('\') + $pathEntries = $userPath -split ';' | Where-Object { $_ -ne '' } + + if ($pathEntries -notcontains $normalizedInstall) { + $newPath = if ([string]::IsNullOrWhiteSpace($userPath)) { + $normalizedInstall + } + else { + "$userPath;$normalizedInstall" + } + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + $env:Path = "$env:Path;$normalizedInstall" + Write-Host "Added to user PATH: $normalizedInstall" + } + else { + Write-Host "Install directory already in user PATH: $normalizedInstall" + } + + Write-Host "Global command available: $BinaryName" +} + +if ($PSBoundParameters.ContainsKey('Help')) { + Write-Usage + exit 0 +} + +$platform = Get-PlatformArtifact +$installDir = (Resolve-Path -Path $Dir).Path +New-Item -ItemType Directory -Force -Path $installDir | Out-Null + +Write-Host "Platform: $platform" +Write-Host "Install directory: $installDir" + +$release = Get-ReleaseJson -VersionTag $Version +$asset = Get-ReleaseAsset -Release $release -Platform $platform +$zipPath = Join-Path $installDir $asset.name + +Write-Host "Asset: $($asset.name)" +Save-FileWithProgress -Url $asset.browser_download_url -Destination $zipPath + +Write-Host 'Extracting...' +Expand-Archive -Path $zipPath -DestinationPath $installDir -Force +Remove-Item -Path $zipPath -Force + +$binaryPath = Join-Path $installDir $BinaryName +if (-not (Test-Path -LiteralPath $binaryPath)) { + throw "Expected binary not found after extraction: $binaryPath" +} + +if (-not $NoGlobal) { + Install-GlobalCommand -InstallDirectory $installDir +} + +Write-Host '' +Write-Host 'Installation complete.' +Write-Host "Run: $BinaryName --help" +if (-not $NoGlobal) { + Write-Host 'Open a new terminal if the command is not available yet.' +} diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index e69de29..a303427 --- a/install.sh +++ b/install.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="ohosvscode/mcp" +BINARY_NAME="arkts-mcp" +GITHUB_API="https://api.github.com/repos/${REPO}" + +INSTALL_DIR="$(pwd)" +VERSION="" +GLOBAL=1 + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + esac +done + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Required command not found: $1" >&2 + exit 1 + fi +} + +detect_platform() { + local os arch + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + case "$os" in + darwin) + case "$arch" in + x86_64 | amd64) echo "darwin-x64" ;; + arm64 | aarch64) echo "darwin-arm64" ;; + *) echo "Unsupported macOS architecture: $arch" >&2; return 1 ;; + esac + ;; + linux) + case "$arch" in + x86_64 | amd64) echo "linux-x64" ;; + arm64 | aarch64) echo "linux-arm64" ;; + *) echo "Unsupported Linux architecture: $arch" >&2; return 1 ;; + esac + ;; + *) + echo "Unsupported operating system: $os" >&2 + echo "On Windows, use install.ps1 instead." >&2 + return 1 + ;; + esac +} + +fetch_release_json() { + local endpoint="$1" + curl -fsSL \ + -H "Accept: application/vnd.github+json" \ + -H "User-Agent: arkts-mcp-installer" \ + "${GITHUB_API}/${endpoint}" +} + +pick_asset_field() { + local platform="$1" + local field="$2" + local release_json="$3" + + if command -v jq >/dev/null 2>&1; then + echo "$release_json" | jq -r --arg platform "$platform" --arg field "$field" ' + .assets[] + | select(.name | test("^" + $platform + "-.*\\.zip$")) + | .[$field] + ' | head -n 1 + return + fi + + require_cmd python3 + echo "$release_json" | python3 -c ' +import json, re, sys +platform, field = sys.argv[1], sys.argv[2] +data = json.load(sys.stdin) +pattern = re.compile(rf"^{re.escape(platform)}-.*\.zip$") +for asset in data.get("assets", []): + if pattern.match(asset.get("name", "")): + print(asset[field]) + break +' "$platform" "$field" +} + +resolve_release_json() { + if [[ -z "$VERSION" ]]; then + fetch_release_json "releases/latest" + return + fi + + local tag normalized candidate release_json + tag="$VERSION" + normalized="${tag#v}" + normalized="${normalized#@arkts/mcp@}" + + for candidate in "$tag" "v${normalized}" "@arkts/mcp@${normalized}"; do + if release_json="$(fetch_release_json "releases/tags/${candidate}" 2>/dev/null || true)"; then + if [[ -n "$release_json" && "$release_json" != *"Not Found"* ]]; then + echo "$release_json" + return + fi + fi + done + + echo "Release not found for version: $VERSION" >&2 + exit 1 +} + +download_with_progress() { + local url="$1" + local dest="$2" + echo "Downloading..." + if command -v curl >/dev/null 2>&1; then + curl -fL --progress-bar -o "$dest" "$url" + echo + return + fi + if command -v wget >/dev/null 2>&1; then + wget --show-progress -O "$dest" "$url" + return + fi + echo "curl or wget is required to download release assets." >&2 + exit 1 +} + +install_global() { + local binary_path="$1" + local bin_dir="${HOME}/.local/bin" + mkdir -p "$bin_dir" + ln -sf "$binary_path" "${bin_dir}/${BINARY_NAME}" + + case ":${PATH}:" in + *":${bin_dir}:"*) ;; + *) + echo + echo "Add this directory to your PATH:" + echo " export PATH=\"${bin_dir}:\$PATH\"" + ;; + esac + + echo "Global command installed: ${bin_dir}/${BINARY_NAME}" +} + +main() { + require_cmd unzip + local platform release_json asset_url asset_name zip_path install_dir_abs binary_path + platform="$(detect_platform)" + install_dir_abs="$(cd "$INSTALL_DIR" && pwd)" + mkdir -p "$install_dir_abs" + + echo "Platform: ${platform}" + echo "Install directory: ${install_dir_abs}" + + release_json="$(resolve_release_json)" + asset_url="$(pick_asset_field "$platform" "browser_download_url" "$release_json")" + asset_name="$(pick_asset_field "$platform" "name" "$release_json")" + + if [[ -z "$asset_url" || "$asset_url" == "null" || -z "$asset_name" || "$asset_name" == "null" ]]; then + echo "No release asset found for platform: ${platform}" >&2 + exit 1 + fi + + zip_path="${install_dir_abs}/${asset_name}" + echo "Asset: ${asset_name}" + download_with_progress "$asset_url" "$zip_path" + + echo "Extracting..." + unzip -oq "$zip_path" -d "$install_dir_abs" + rm -f "$zip_path" + + binary_path="${install_dir_abs}/${BINARY_NAME}" + if [[ ! -f "$binary_path" ]]; then + echo "Expected binary not found after extraction: ${binary_path}" >&2 + exit 1 + fi + chmod +x "$binary_path" + + if [[ "$GLOBAL" -eq 1 ]]; then + install_global "$binary_path" + fi + + echo + echo "Installation complete." + echo "Run: ${BINARY_NAME} --help" +} + +main "$@" From 2cf4cb755949e8fb5a4e1dd09c559b15e6cd626f Mon Sep 17 00:00:00 2001 From: groupguanfang <1203970284@qq.com> Date: Wed, 3 Jun 2026 07:56:31 +0800 Subject: [PATCH 4/6] feat: update --- install.ps1 | 11 ++++++++++- install.sh | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 4298dc9..1eddee2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -46,7 +46,16 @@ function Get-ReleaseJson { param([string] $VersionTag) if ([string]::IsNullOrWhiteSpace($VersionTag)) { - return Invoke-RestMethod -Uri "$GithubApi/releases/latest" -Headers $Headers + try { + return Invoke-RestMethod -Uri "$GithubApi/releases/latest" -Headers $Headers + } + catch { + $releases = @(Invoke-RestMethod -Uri "$GithubApi/releases?per_page=1" -Headers $Headers) + if ($releases.Count -eq 0) { + throw 'No releases found' + } + return $releases[0] + } } $normalized = $VersionTag.TrimStart('v') diff --git a/install.sh b/install.sh index a303427..8445a55 100755 --- a/install.sh +++ b/install.sh @@ -124,7 +124,21 @@ for asset in data.get("assets", []): resolve_release_json() { if [[ -z "$VERSION" ]]; then - fetch_release_json "releases/latest" + local release_json releases_list + release_json="$(fetch_release_json "releases/latest" 2>/dev/null || true)" + if [[ -n "$release_json" ]]; then + echo "$release_json" + return + fi + + # GitHub /releases/latest returns 404 when the newest release is a prerelease. + releases_list="$(fetch_release_json "releases?per_page=1")" + if command -v jq >/dev/null 2>&1; then + echo "$releases_list" | jq -c '.[0]' + return + fi + require_cmd python3 + echo "$releases_list" | python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)[0]))' return fi From 1712bb3ae3c8799ded69dc38369d4b4dd409746c Mon Sep 17 00:00:00 2001 From: groupguanfang <1203970284@qq.com> Date: Wed, 3 Jun 2026 08:11:11 +0800 Subject: [PATCH 5/6] feat: update --- .vscode/settings.json | 1 + package.json | 1 + pnpm-lock.yaml | 3 + src/bin.ts | 36 ++++-- src/installer.ts | 287 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 src/installer.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e1d624b..10eb46c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,6 +52,7 @@ "cfworker", "cppjieba", "harmonyos", + "LOCALAPPDATA", "minisearch", "modelcontextprotocol", "Naily", diff --git a/package.json b/package.json index 9949e90..e4b6b7e 100755 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@vstils/core": "^0.0.8", "ajv": "^8.18.0", "axios": "^1.15.0", + "cac": "^7.0.0", "minisearch": "^7.2.0", "nodejieba": "^3.5.8", "source-map-support": "^0.5.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7a67c5..c2cd707 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: axios: specifier: ^1.15.0 version: 1.15.0 + cac: + specifier: ^7.0.0 + version: 7.0.0 minisearch: specifier: ^7.2.0 version: 7.2.0 diff --git a/src/bin.ts b/src/bin.ts index a8f976e..8e15aad 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,15 +1,33 @@ #!/usr/bin/env node -import process from 'node:process' import { StdioServerTransport } from '@modelcontextprotocol/server' +import cac from 'cac' +import { version } from '../package.json' import { createArktsMcpServer } from './index' +import { uninstallArktsMcp, updateArktsMcp } from './installer' import 'source-map-support/register' -async function main(): Promise { - const server = await createArktsMcpServer() - await server.connect(new StdioServerTransport()) -} +const cli = cac('arkts-mcp') -main().catch((error) => { - console.error('Fatal error in main():', error) - process.exit(1) -}) +cli + .command('[root]', 'Start the arkTS MCP server.') + .action(async () => { + const server = await createArktsMcpServer() + await server.connect(new StdioServerTransport()) + }) + +cli + .command('update', 'Update the arkTS MCP server.') + .option('--version ', 'Install a specific release version') + .action(async (options: { version?: string }) => { + await updateArktsMcp({ version: options.version }) + }) + +cli + .command('uninstall', 'Uninstall the arkTS MCP server.') + .action(async () => { + await uninstallArktsMcp() + }) + +cli.help() +cli.version(version) +cli.parse() diff --git a/src/installer.ts b/src/installer.ts new file mode 100644 index 0000000..36eae78 --- /dev/null +++ b/src/installer.ts @@ -0,0 +1,287 @@ +/* eslint-disable no-console */ +import type { Buffer } from 'node:buffer' +import child_process from 'node:child_process' +import fs from 'node:fs' +import fsp from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { pipeline } from 'node:stream/promises' +import axios from 'axios' +import { version as currentVersion } from '../package.json' + +const REPO = 'ohosvscode/mcp' +const GITHUB_API = `https://api.github.com/repos/${REPO}` +const GITHUB_HEADERS = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'arkts-mcp-installer', +} + +interface GitHubReleaseAsset { + name: string + browser_download_url: string +} + +interface GitHubRelease { + tag_name: string + assets: GitHubReleaseAsset[] +} + +export interface UpdateOptions { + version?: string +} + +function binaryName() { + return process.platform === 'win32' ? 'arkts-mcp.exe' : 'arkts-mcp' +} + +function run(command: string, args: string[]) { + return new Promise((resolve, reject) => { + child_process.spawn(command, args, { stdio: 'inherit' }) + .on('error', reject) + .on('close', code => code === 0 ? resolve() : reject(new Error(`Command failed: ${command} ${args.join(' ')}`))) + }) +} + +function detectPlatform() { + const { platform, arch } = process + if (platform === 'darwin') { + if (arch === 'x64') return 'darwin-x64' + if (arch === 'arm64') return 'darwin-arm64' + } + if (platform === 'linux') { + if (arch === 'x64') return 'linux-x64' + if (arch === 'arm64') return 'linux-arm64' + } + if (platform === 'win32') { + if (arch === 'x64') return 'win-x64' + if (arch === 'arm64') return 'win-arm64' + } + throw new Error(`Unsupported platform: ${platform} ${arch}`) +} + +async function fetchJson(url: string): Promise { + const response = await axios.get(url, { headers: GITHUB_HEADERS }) + return response.data +} + +async function fetchRelease(version?: string): Promise { + if (!version) { + try { + return await fetchJson(`${GITHUB_API}/releases/latest`) + } + catch { + const releases = await fetchJson(`${GITHUB_API}/releases?per_page=1`) + const latest = releases[0] + if (!latest) throw new Error('No releases found') + return latest + } + } + + const normalized = version.replace(/^v/, '').replace(/^@arkts\/mcp@/, '') + const candidates = [version, `v${normalized}`, `@arkts/mcp@${normalized}`] + for (const candidate of candidates) { + try { + return await fetchJson(`${GITHUB_API}/releases/tags/${candidate}`) + } + catch { + continue + } + } + throw new Error(`Release not found for version: ${version}`) +} + +function normalizeVersion(tag: string) { + return tag.replace(/^v/, '').replace(/^@arkts\/mcp@/, '') +} + +function pickAsset(release: GitHubRelease, platform: string) { + const pattern = new RegExp(`^${platform.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-.*\\.zip$`) + const asset = release.assets.find(item => pattern.test(item.name)) + if (!asset) throw new Error(`No release asset found for platform: ${platform}`) + return asset +} + +async function downloadWithProgress(url: string, destination: string) { + const response = await axios.get(url, { + responseType: 'stream', + headers: GITHUB_HEADERS, + }) + const total = Number(response.headers['content-length'] ?? 0) + let downloaded = 0 + + process.stderr.write('Downloading...') + response.data.on('data', (chunk: Buffer) => { + downloaded += chunk.length + if (total > 0) { + const percent = Math.floor((downloaded / total) * 100) + process.stderr.write(`\rDownloading... ${percent}%`) + } + }) + + await pipeline(response.data, fs.createWriteStream(destination)) + process.stderr.write('\n') +} + +async function extractArchive(zipPath: string, destination: string) { + if (process.platform === 'win32') { + const escapedZip = zipPath.replace(/'/g, '\'\'') + const escapedDest = destination.replace(/'/g, '\'\'') + await run('powershell', [ + '-NoProfile', + '-Command', + `Expand-Archive -Path '${escapedZip}' -DestinationPath '${escapedDest}' -Force`, + ]) + return + } + + await run('unzip', ['-oq', zipPath, '-d', destination]) +} + +function resolveInstallDirectory() { + const execPath = fs.realpathSync(process.execPath) + const base = path.basename(execPath).toLowerCase() + if (base === 'node' || base === 'node.exe') { + throw new Error('This command is only supported for standalone installations installed via install.sh or install.ps1.') + } + return path.dirname(execPath) +} + +function globalCommandPath() { + if (process.platform === 'win32') { + return path.join(process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local'), 'bin', binaryName()) + } + return path.join(os.homedir(), '.local', 'bin', binaryName()) +} + +async function refreshGlobalCommand(installDir: string) { + const binaryPath = path.join(installDir, binaryName()) + + if (process.platform === 'win32') { + await addWindowsInstallDirToPath(installDir) + console.log(`Global command available: ${binaryName()}`) + return + } + + const linkPath = globalCommandPath() + await fsp.mkdir(path.dirname(linkPath), { recursive: true }) + try { + await fsp.unlink(linkPath) + } + catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error + } + await fsp.symlink(binaryPath, linkPath) + console.log(`Global command installed: ${linkPath}`) +} + +async function addWindowsInstallDirToPath(installDir: string) { + const script = ` +$installDir = '${installDir.replace(/'/g, '\'\'')}' +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +$entries = @() +if ($userPath) { $entries = $userPath -split ';' | Where-Object { $_ -ne '' } } +if ($entries -notcontains $installDir) { + $newPath = if ($userPath) { "$userPath;$installDir" } else { $installDir } + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') +} +` + await run('powershell', ['-NoProfile', '-Command', script]) +} + +async function removeWindowsInstallDirFromPath(installDir: string) { + const script = ` +$installDir = '${installDir.replace(/'/g, '\'\'')}' +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +if (-not $userPath) { return } +$entries = $userPath -split ';' | Where-Object { $_ -ne '' -and $_ -ne $installDir } +[Environment]::SetEnvironmentVariable('Path', ($entries -join ';'), 'User') +` + await run('powershell', ['-NoProfile', '-Command', script]) +} + +async function removeGlobalCommand(installDir: string) { + if (process.platform === 'win32') { + await removeWindowsInstallDirFromPath(installDir) + console.log(`Removed install directory from user PATH: ${installDir}`) + return + } + + const linkPath = globalCommandPath() + if (!fs.existsSync(linkPath)) return + + const binaryPath = path.join(installDir, binaryName()) + const linkTarget = fs.realpathSync(linkPath) + if (linkTarget === binaryPath) { + await fsp.unlink(linkPath) + console.log(`Removed global command: ${linkPath}`) + } +} + +async function removeInstalledFiles(installDir: string) { + const targets = [ + path.join(installDir, binaryName()), + path.join(installDir, 'lib'), + path.join(installDir, 'assets'), + ] + + for (const target of targets) { + await fsp.rm(target, { recursive: true, force: true }) + console.log(`Removed: ${target}`) + } +} + +async function installRelease(installDir: string, version?: string) { + const platform = detectPlatform() + const release = await fetchRelease(version) + const asset = pickAsset(release, platform) + + console.log(`Platform: ${platform}`) + console.log(`Install directory: ${installDir}`) + console.log(`Asset: ${asset.name}`) + + await fsp.mkdir(installDir, { recursive: true }) + const zipPath = path.join(installDir, asset.name) + await downloadWithProgress(asset.browser_download_url, zipPath) + + console.log('Extracting...') + await extractArchive(zipPath, installDir) + await fsp.rm(zipPath, { force: true }) + + const binaryPath = path.join(installDir, binaryName()) + if (!fs.existsSync(binaryPath)) { + throw new Error(`Expected binary not found after extraction: ${binaryPath}`) + } + if (process.platform !== 'win32') { + await fsp.chmod(binaryPath, 0o755) + } +} + +export async function updateArktsMcp(options: UpdateOptions = {}) { + const installDir = resolveInstallDirectory() + const release = await fetchRelease(options.version) + const nextVersion = normalizeVersion(release.tag_name) + + if (!options.version && nextVersion === currentVersion) { + console.log(`Already up to date (${currentVersion}).`) + return + } + + await installRelease(installDir, options.version) + await refreshGlobalCommand(installDir) + + console.log('') + console.log(`Updated to ${nextVersion}.`) + console.log(`Run: ${binaryName()} --help`) +} + +export async function uninstallArktsMcp() { + const installDir = resolveInstallDirectory() + + console.log(`Install directory: ${installDir}`) + await removeGlobalCommand(installDir) + await removeInstalledFiles(installDir) + + console.log('') + console.log('Uninstallation complete.') +} From 40a13b638bb38b71074ec49feeefea8f1c24841a Mon Sep 17 00:00:00 2001 From: groupguanfang <1203970284@qq.com> Date: Wed, 3 Jun 2026 08:15:08 +0800 Subject: [PATCH 6/6] feat: update --- .changeset/loud-rings-share.md | 5 ++ README.md | 146 ++++++++++++++++++++++++++++++++- install.ps1 | 1 + install.sh | 1 + 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 .changeset/loud-rings-share.md diff --git a/.changeset/loud-rings-share.md b/.changeset/loud-rings-share.md new file mode 100644 index 0000000..3ee2900 --- /dev/null +++ b/.changeset/loud-rings-share.md @@ -0,0 +1,5 @@ +--- +"@arkts/mcp": patch +--- + +feat: update README diff --git a/README.md b/README.md index 60f822b..a030f10 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,147 @@ # ArkTS MCP Server -ArkTS MCP 服务器,正在开发中。 +面向 [ArkTS](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts) 的 [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 服务器,可在 Cursor 等 AI 工具中提供 HarmonyOS / OpenHarmony 文档检索等能力。 + +## 功能 + +- 启动标准 MCP stdio 服务,供编辑器与 Agent 调用 +- 提供 HarmonyOS 开发者文档目录检索工具 `docs_get_catalog_tree` + - 支持中文 / 英文目录 + - 支持全文搜索与完整目录获取 + +## 安装 + +### 独立可执行文件(推荐) + +从 [GitHub Releases](https://github.com/ohosvscode/mcp/releases) 下载对应平台压缩包,或使用安装脚本。 + +**macOS / Linux** + +国内网络(jsDelivr 镜像,推荐): + +```bash +curl -fsSL https://cdn.jsdelivr.net/gh/ohosvscode/mcp@main/install.sh | bash +``` + +官方源: + +```bash +curl -fsSL https://raw.githubusercontent.com/ohosvscode/mcp/main/install.sh | bash +``` + +**Windows (PowerShell)** + +国内网络(jsDelivr 镜像,推荐): + +```powershell +irm https://cdn.jsdelivr.net/gh/ohosvscode/mcp@main/install.ps1 | iex +``` + +官方源: + +```powershell +irm https://raw.githubusercontent.com/ohosvscode/mcp/main/install.ps1 | iex +``` + +> 安装脚本本身可通过 jsDelivr 加速获取;Release 中的可执行文件压缩包仍从 GitHub Releases 下载。若下载 Release 较慢,可手动从 [Releases 页面](https://github.com/ohosvscode/mcp/releases) 选择对应平台 zip 后解压,再执行 `arkts-mcp` 或使用 `--no-global` / `-NoGlobal` 仅本地安装。 + +安装脚本会: + +1. 自动检测平台(`darwin-x64`、`darwin-arm64`、`linux-x64`、`linux-arm64`、`win-x64`、`win-arm64`) +2. 下载最新 Release 压缩包(含 pre-release) +3. 解压到当前目录 +4. 注册全局命令 `arkts-mcp` + +可选参数: + +```bash +./install.sh --version 0.0.1-alpha.2 +./install.sh --dir ~/apps/arkts-mcp --no-global +``` + +```powershell +.\install.ps1 -Version 0.0.1-alpha.2 +.\install.ps1 -Dir "$env:LOCALAPPDATA\arkts-mcp" -NoGlobal +``` + +### npm + +```bash +pnpm add -g @arkts/mcp +# 或 +npx @arkts/mcp +``` + +## 在 Cursor 中使用 + +在 MCP 配置中加入 `arkts-mcp`: + +```json +{ + "mcpServers": { + "arkts": { + "command": "arkts-mcp" + } + } +} +``` + +若使用 npm 安装且命令不在 PATH 中,可改为: + +```json +{ + "mcpServers": { + "arkts": { + "command": "npx", + "args": ["@arkts/mcp"] + } + } +} +``` + +本地开发时可指向构建产物: + +```json +{ + "mcpServers": { + "arkts": { + "command": "node", + "args": ["dist/bin.mjs"] + } + } +} +``` + +## CLI + +| 命令 | 说明 | +| --- | --- | +| `arkts-mcp` | 启动 MCP 服务器(stdio) | +| `arkts-mcp update` | 更新到最新 Release | +| `arkts-mcp update --version ` | 更新到指定版本 | +| `arkts-mcp uninstall` | 卸载全局命令并删除安装文件 | +| `arkts-mcp --help` | 查看帮助 | +| `arkts-mcp --version` | 查看版本 | + +`update` / `uninstall` 仅适用于通过 `install.sh` / `install.ps1` 安装的独立可执行文件。 + +## 开发 + +环境要求见 [package.json](./package.json) 中的 `packageManager` 与 [.node-version](./.node-version)。 + +```bash +pnpm install +pnpm build # 构建 npm 包,输出到 dist/ +pnpm build --build-exe # 同时构建独立可执行文件,输出到 target/ +pnpm test # 运行测试 +``` + +独立可执行文件构建完成后,产物位于 `target/`,包含: + +- `arkts-mcp`(Windows 为 `arkts-mcp.exe`) +- `lib/`(原生依赖,如 nodejieba) +- `assets/`(如图标等资源) + +## 许可证 + +[MIT](./LICENSE) diff --git a/install.ps1 b/install.ps1 index 1eddee2..dfceaa7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -28,6 +28,7 @@ Options: -NoGlobal Do not add the install directory to the user PATH Examples: + irm https://cdn.jsdelivr.net/gh/$Repo/main/install.ps1 | iex irm https://raw.githubusercontent.com/$Repo/main/install.ps1 | iex .\install.ps1 -Version 0.0.1-alpha.2 "@ diff --git a/install.sh b/install.sh index 8445a55..c8545e5 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,7 @@ Options: -h, --help Show this help Examples: + curl -fsSL https://cdn.jsdelivr.net/gh/${REPO}/main/install.sh | bash curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash ./install.sh --version 0.0.1-alpha.2 EOF