Fix Cursor MCP deeplinks: encode transport-only config so command is not empty.
This commit is contained in:
1 parent
4c7e3ff720
commit
694de7d1d7
5 files changed
+68
-22
No files matched your search
+50
-16
@@ -1,6 +1,8 @@
|
||||
# Builds a Cursor MCP install deeplink from a server name and config object (hashtable/JSON).
|
||||
# Usage: . scripts\cursor-deeplink.ps1; New-CursorDeeplink -Name wordcraft -Config @{ command='...'; args=@(...) }
|
||||
# Or: scripts\cursor-deeplink.ps1 -Name wordcraft -ConfigJson '{...}'
|
||||
# Builds a Cursor MCP install deeplink.
|
||||
# config = base64(JSON.stringify({command, args, ...})) -- transport only
|
||||
# name = server key (separate query param)
|
||||
# Wrapping as { serverName: transport } makes Cursor show command: "".
|
||||
# Usage: . scripts\cursor-deeplink.ps1; New-CursorDeeplink -ServerName wordcraft -Config @{ command='...'; args=@('mcp') }
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$ConfigJson
|
||||
@@ -8,26 +10,58 @@ param(
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
function New-CursorDeeplink([string]$ServerName, $Config) {
|
||||
# Config = server transport object (command/args/...) OR { serverName = transport }.
|
||||
# Deeplink uses name= + base64(JSON of the transport object keyed by name), per Cursor docs.
|
||||
$command = $null
|
||||
$argList = @()
|
||||
$extra = @{}
|
||||
|
||||
if ($Config -is [string]) {
|
||||
$obj = $Config | ConvertFrom-Json
|
||||
$Config = $Config | ConvertFrom-Json
|
||||
}
|
||||
|
||||
if ($Config -is [hashtable]) {
|
||||
if ($Config.ContainsKey($ServerName) -and $Config[$ServerName] -is [hashtable]) {
|
||||
$Config = $Config[$ServerName]
|
||||
}
|
||||
$command = [string]$Config['command']
|
||||
if ($Config.ContainsKey('args')) { $argList = @($Config['args']) }
|
||||
foreach ($k in $Config.Keys) {
|
||||
if ($k -ne 'command' -and $k -ne 'args') { $extra[$k] = $Config[$k] }
|
||||
}
|
||||
}
|
||||
else {
|
||||
$obj = $Config
|
||||
# PSCustomObject
|
||||
$props = @($Config.PSObject.Properties.Name)
|
||||
if ($props.Count -eq 1 -and $props[0] -eq $ServerName) {
|
||||
$Config = $Config.$ServerName
|
||||
$props = @($Config.PSObject.Properties.Name)
|
||||
}
|
||||
$command = [string]$Config.command
|
||||
if ($null -ne $Config.args) { $argList = @($Config.args) }
|
||||
foreach ($p in $Config.PSObject.Properties) {
|
||||
if ($p.Name -ne 'command' -and $p.Name -ne 'args') { $extra[$p.Name] = $p.Value }
|
||||
}
|
||||
}
|
||||
# Normalize to { name: transport }
|
||||
$props = @($obj.PSObject.Properties.Name)
|
||||
if ($props.Count -eq 1 -and $props[0] -eq $ServerName) {
|
||||
$payload = $obj
|
||||
|
||||
# Forward slashes: valid on Windows, safer through JSON/URL pipelines.
|
||||
if ($command -match '^[A-Za-z]:\\') {
|
||||
$command = $command -replace '\\', '/'
|
||||
}
|
||||
elseif ($obj.command -or $obj.PSObject.Properties['command']) {
|
||||
$payload = @{ $ServerName = $obj }
|
||||
|
||||
$transport = [ordered]@{ command = $command; args = @($argList) }
|
||||
foreach ($k in $extra.Keys) { $transport[$k] = $extra[$k] }
|
||||
|
||||
$jsonPath = [IO.Path]::GetTempFileName()
|
||||
try {
|
||||
# Write via .NET so Python can re-dump with JSON.stringify-compatible compact form.
|
||||
$psJson = ($transport | ConvertTo-Json -Compress -Depth 20)
|
||||
[IO.File]::WriteAllText($jsonPath, $psJson, $Utf8NoBom)
|
||||
$json = & python -c "import json,sys; print(json.dumps(json.load(open(sys.argv[1],encoding='utf-8')),separators=(',',':'),ensure_ascii=False))" $jsonPath
|
||||
if (-not $json) { $json = $psJson }
|
||||
}
|
||||
else {
|
||||
$payload = $obj
|
||||
finally {
|
||||
Remove-Item -LiteralPath $jsonPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
$json = ($payload | ConvertTo-Json -Compress -Depth 20)
|
||||
|
||||
$bytes = $Utf8NoBom.GetBytes($json)
|
||||
$b64 = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
||||
return "cursor://anysphere.cursor-deeplink/mcp/install?name=$([Uri]::EscapeDataString($ServerName))&config=$b64"
|
||||
|
||||
@@ -13,7 +13,14 @@ python3 -c '
|
||||
import base64, json, sys, urllib.parse
|
||||
name, raw = sys.argv[1], sys.argv[2]
|
||||
transport = json.loads(raw)
|
||||
payload = {name: transport}
|
||||
b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")).decode("ascii").rstrip("=")
|
||||
# Unwrap { name: transport } if the caller passed it that way.
|
||||
if set(transport.keys()) == {name} and isinstance(transport[name], dict):
|
||||
transport = transport[name]
|
||||
cmd = transport.get("command") or ""
|
||||
if len(cmd) >= 3 and cmd[1] == ":" and "\\" in cmd:
|
||||
transport = dict(transport)
|
||||
transport["command"] = cmd.replace("\\\\", "/").replace("\\", "/")
|
||||
# config = transport only; name= is a separate query param.
|
||||
b64 = base64.urlsafe_b64encode(json.dumps(transport, separators=(",", ":")).encode("utf-8")).decode("ascii").rstrip("=")
|
||||
print(f"cursor://anysphere.cursor-deeplink/mcp/install?name={urllib.parse.quote(name)}&config={b64}")
|
||||
' "$name" "$config"
|
||||
@@ -56,8 +56,11 @@ for a in man["server"]["mcp_config"].get("args") or []:
|
||||
args.append("${userHome}/Documents/Claude")
|
||||
else:
|
||||
args.append(s)
|
||||
payload = {sk: {"command": cmd, "args": args}}
|
||||
b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode().rstrip("=")
|
||||
# Forward slashes; config = transport only (name= is separate).
|
||||
if len(cmd) >= 3 and cmd[1] == ":" and "\\" in cmd:
|
||||
cmd = cmd.replace("\\", "/")
|
||||
transport = {"command": cmd, "args": args}
|
||||
b64 = base64.urlsafe_b64encode(json.dumps(transport, separators=(",", ":")).encode()).decode().rstrip("=")
|
||||
link = f"cursor://anysphere.cursor-deeplink/mcp/install?name={urllib.parse.quote(sk)}&config={b64}"
|
||||
print(f"[Add to Cursor]({link})")
|
||||
PY
|
||||
|
||||
Reference in new issue
Block a user