gax-cli 0.4.0__py3-none-any.whl
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.
- gax/__init__.py +3 -0
- gax/adapters/__init__.py +3 -0
- gax/adapters/base.py +25 -0
- gax/adapters/exec_adapter.py +261 -0
- gax/adapters/http_adapter.py +33 -0
- gax/adapters/k8s_adapter.py +338 -0
- gax/adapters/mcp_bridge.py +108 -0
- gax/adapters/mock_adapter.py +49 -0
- gax/audit.py +54 -0
- gax/caps.py +91 -0
- gax/cli.py +722 -0
- gax/client.py +58 -0
- gax/compliance.py +71 -0
- gax/config/oauth_providers.yaml +21 -0
- gax/config/policy.rego +20 -0
- gax/config/policy.yaml +59 -0
- gax/daemon.py +187 -0
- gax/envelope.py +64 -0
- gax/executor.py +151 -0
- gax/macaroons_cap.py +80 -0
- gax/manifests/aws.s3.list.yaml +20 -0
- gax/manifests/demo.echo.yaml +20 -0
- gax/manifests/gh.pr.list.yaml +41 -0
- gax/manifests/gh.pr.view.yaml +30 -0
- gax/manifests/jira.issue.get.yaml +18 -0
- gax/manifests/k8s.deployment.scale.yaml +35 -0
- gax/manifests/k8s.pod.delete.yaml +30 -0
- gax/manifests/kubectl.get.pods.yaml +20 -0
- gax/manifests/mcp.github.list_pulls.yaml +29 -0
- gax/manifests/pet_findpetsbystatus.yaml +19 -0
- gax/manifests/pet_getpetbyid.yaml +19 -0
- gax/mcp_client.py +110 -0
- gax/mcp_server.py +299 -0
- gax/oauth.py +184 -0
- gax/onboarding.py +397 -0
- gax/opa_policy.py +46 -0
- gax/openapi_gen.py +96 -0
- gax/otel_audit.py +50 -0
- gax/paths.py +46 -0
- gax/plan.py +151 -0
- gax/policy.py +33 -0
- gax/policy_bundle.py +64 -0
- gax/profiles/github/gh.issue.list.yaml +25 -0
- gax/profiles/github/gh.issue.view.yaml +23 -0
- gax/profiles/github/gh.pr.comment.yaml +30 -0
- gax/profiles/github/gh.pr.merge.yaml +29 -0
- gax/profiles/github/gh.run.list.yaml +22 -0
- gax/profiles/k8s/k8s.deployment.list.yaml +17 -0
- gax/profiles/k8s/k8s.deployment.restart.yaml +25 -0
- gax/profiles/k8s/k8s.namespace.delete.yaml +22 -0
- gax/profiles/k8s/k8s.pod.describe.yaml +22 -0
- gax/profiles/k8s/k8s.pod.logs.yaml +28 -0
- gax/profiles/k8s/k8s.service.list.yaml +17 -0
- gax/profiles.py +143 -0
- gax/projection.py +42 -0
- gax/registry.py +130 -0
- gax/schemas/envelope.v1.json +46 -0
- gax/secrets.py +49 -0
- gax/side_effects.py +67 -0
- gax/spiffe.py +32 -0
- gax/vault.py +69 -0
- gax_cli-0.4.0.dist-info/METADATA +164 -0
- gax_cli-0.4.0.dist-info/RECORD +66 -0
- gax_cli-0.4.0.dist-info/WHEEL +5 -0
- gax_cli-0.4.0.dist-info/entry_points.txt +4 -0
- gax_cli-0.4.0.dist-info/top_level.txt +1 -0
gax/macaroons_cap.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from gax.caps import jwt_secret
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _sign(data: bytes, key: bytes) -> str:
|
|
14
|
+
return base64.urlsafe_b64encode(hmac.new(key, data, hashlib.sha256).digest()).decode().rstrip("=")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def mint_macaroon(
|
|
18
|
+
*,
|
|
19
|
+
tenant_id: str,
|
|
20
|
+
subject: str,
|
|
21
|
+
commands: list[str],
|
|
22
|
+
scopes: list[str],
|
|
23
|
+
ttl_seconds: int = 3600,
|
|
24
|
+
caveats: list[dict[str, Any]] | None = None,
|
|
25
|
+
) -> str:
|
|
26
|
+
"""HMAC-chained capability (macaroon-style attenuation, MVP)."""
|
|
27
|
+
root_key = jwt_secret().encode()
|
|
28
|
+
now = int(time.time())
|
|
29
|
+
payload: dict[str, Any] = {
|
|
30
|
+
"v": 1,
|
|
31
|
+
"tenant_id": tenant_id,
|
|
32
|
+
"sub": subject,
|
|
33
|
+
"commands": commands,
|
|
34
|
+
"scopes": scopes,
|
|
35
|
+
"iat": now,
|
|
36
|
+
"exp": now + ttl_seconds,
|
|
37
|
+
"caveats": caveats or [],
|
|
38
|
+
}
|
|
39
|
+
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
40
|
+
sig = _sign(body, root_key)
|
|
41
|
+
for caveat in caveats or []:
|
|
42
|
+
sig = _sign(f"{sig}|{json.dumps(caveat, sort_keys=True)}".encode(), root_key)
|
|
43
|
+
blob = base64.urlsafe_b64encode(json.dumps({"p": payload, "s": sig}).encode()).decode()
|
|
44
|
+
return f"gaxm_{blob}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def verify_macaroon(token: str) -> dict[str, Any]:
|
|
48
|
+
if token.startswith("gaxm_"):
|
|
49
|
+
token = token[5:]
|
|
50
|
+
try:
|
|
51
|
+
pad = "=" * (-len(token) % 4)
|
|
52
|
+
raw = json.loads(base64.urlsafe_b64decode(token + pad))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise ValueError("invalid macaroon") from e
|
|
55
|
+
payload = raw.get("p") or {}
|
|
56
|
+
sig = raw.get("s", "")
|
|
57
|
+
root_key = jwt_secret().encode()
|
|
58
|
+
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
59
|
+
expected = _sign(body, root_key)
|
|
60
|
+
for caveat in payload.get("caveats") or []:
|
|
61
|
+
expected = _sign(f"{expected}|{json.dumps(caveat, sort_keys=True)}".encode(), root_key)
|
|
62
|
+
if not hmac.compare_digest(expected, sig):
|
|
63
|
+
raise ValueError("macaroon signature mismatch")
|
|
64
|
+
if int(payload.get("exp", 0)) < time.time():
|
|
65
|
+
raise ValueError("macaroon expired")
|
|
66
|
+
return payload
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def macaroon_allows_command(claims: dict[str, Any], command: str) -> bool:
|
|
70
|
+
allowed = claims.get("commands") or []
|
|
71
|
+
if "*" in allowed:
|
|
72
|
+
return True
|
|
73
|
+
return command in allowed
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def macaroon_allows_scopes(claims: dict[str, Any], required: list[str]) -> bool:
|
|
77
|
+
held = set(claims.get("scopes") or [])
|
|
78
|
+
if "*" in held:
|
|
79
|
+
return True
|
|
80
|
+
return set(required).issubset(held)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
command: aws.s3.list
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: List S3 buckets (mock MVP)
|
|
4
|
+
category: aws
|
|
5
|
+
adapter: mock
|
|
6
|
+
required_scopes:
|
|
7
|
+
- aws:s3:read
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
prefix:
|
|
14
|
+
type: string
|
|
15
|
+
default: ""
|
|
16
|
+
output_schema:
|
|
17
|
+
type: object
|
|
18
|
+
properties:
|
|
19
|
+
buckets:
|
|
20
|
+
type: array
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
command: demo.echo
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: Echo arguments (demo / testing without gh)
|
|
4
|
+
category: demo
|
|
5
|
+
adapter: mock
|
|
6
|
+
required_scopes:
|
|
7
|
+
- demo:echo
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
message:
|
|
14
|
+
type: string
|
|
15
|
+
default: hello
|
|
16
|
+
output_schema:
|
|
17
|
+
type: object
|
|
18
|
+
properties:
|
|
19
|
+
echo:
|
|
20
|
+
type: string
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
command: gh.pr.list
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: List pull requests for a repository
|
|
4
|
+
category: github
|
|
5
|
+
adapter: exec
|
|
6
|
+
backend:
|
|
7
|
+
- gh
|
|
8
|
+
- pr
|
|
9
|
+
- list
|
|
10
|
+
required_scopes:
|
|
11
|
+
- github:pull_request:read
|
|
12
|
+
side_effects: read
|
|
13
|
+
idempotent: true
|
|
14
|
+
input_schema:
|
|
15
|
+
type: object
|
|
16
|
+
required: [repo]
|
|
17
|
+
properties:
|
|
18
|
+
repo:
|
|
19
|
+
type: string
|
|
20
|
+
description: "owner/name"
|
|
21
|
+
limit:
|
|
22
|
+
type: integer
|
|
23
|
+
default: 30
|
|
24
|
+
state:
|
|
25
|
+
type: string
|
|
26
|
+
enum: [open, closed, merged, all]
|
|
27
|
+
default: open
|
|
28
|
+
output_schema:
|
|
29
|
+
type: object
|
|
30
|
+
properties:
|
|
31
|
+
items:
|
|
32
|
+
type: array
|
|
33
|
+
items:
|
|
34
|
+
type: object
|
|
35
|
+
properties:
|
|
36
|
+
number: { type: integer }
|
|
37
|
+
title: { type: string }
|
|
38
|
+
state: { type: string }
|
|
39
|
+
url: { type: string }
|
|
40
|
+
author: { type: string }
|
|
41
|
+
draft: { type: boolean }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
command: gh.pr.view
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: View a single pull request
|
|
4
|
+
category: github
|
|
5
|
+
adapter: exec
|
|
6
|
+
backend:
|
|
7
|
+
- gh
|
|
8
|
+
- pr
|
|
9
|
+
- view
|
|
10
|
+
required_scopes:
|
|
11
|
+
- github:pull_request:read
|
|
12
|
+
side_effects: read
|
|
13
|
+
idempotent: true
|
|
14
|
+
input_schema:
|
|
15
|
+
type: object
|
|
16
|
+
required: [repo, number]
|
|
17
|
+
properties:
|
|
18
|
+
repo:
|
|
19
|
+
type: string
|
|
20
|
+
number:
|
|
21
|
+
type: integer
|
|
22
|
+
output_schema:
|
|
23
|
+
type: object
|
|
24
|
+
properties:
|
|
25
|
+
number: { type: integer }
|
|
26
|
+
title: { type: string }
|
|
27
|
+
body: { type: string }
|
|
28
|
+
state: { type: string }
|
|
29
|
+
url: { type: string }
|
|
30
|
+
author: { type: string }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
command: jira.issue.get
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: Get Jira issue by key (mock MVP)
|
|
4
|
+
category: jira
|
|
5
|
+
adapter: mock
|
|
6
|
+
required_scopes:
|
|
7
|
+
- jira:issue:read
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
required: [issue_key]
|
|
13
|
+
properties:
|
|
14
|
+
issue_key:
|
|
15
|
+
type: string
|
|
16
|
+
description: e.g. PROJ-123
|
|
17
|
+
output_schema:
|
|
18
|
+
type: object
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
command: k8s.deployment.scale
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: Scale a deployment to N replicas. Mutating; scaling to 0 takes a service down.
|
|
4
|
+
category: kubernetes
|
|
5
|
+
adapter: k8s
|
|
6
|
+
required_scopes:
|
|
7
|
+
- k8s:deployments:write
|
|
8
|
+
side_effects: write
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
namespace:
|
|
14
|
+
type: string
|
|
15
|
+
description: Kubernetes namespace (DNS-1123)
|
|
16
|
+
deployment:
|
|
17
|
+
type: string
|
|
18
|
+
description: Deployment name (DNS-1123)
|
|
19
|
+
replicas:
|
|
20
|
+
type: integer
|
|
21
|
+
description: Desired replica count (>= 0)
|
|
22
|
+
dry_run:
|
|
23
|
+
type: boolean
|
|
24
|
+
description: Validate server-side without scaling (kubectl --dry-run=server)
|
|
25
|
+
required:
|
|
26
|
+
- deployment
|
|
27
|
+
- replicas
|
|
28
|
+
output_schema:
|
|
29
|
+
type: object
|
|
30
|
+
properties:
|
|
31
|
+
scaled: { type: boolean }
|
|
32
|
+
deployment: { type: string }
|
|
33
|
+
namespace: { type: string }
|
|
34
|
+
replicas: { type: integer }
|
|
35
|
+
dry_run: { type: boolean }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
command: k8s.pod.delete
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: Delete a pod in a namespace. Destructive; requires a capability with a destructive ceiling.
|
|
4
|
+
category: kubernetes
|
|
5
|
+
adapter: k8s
|
|
6
|
+
required_scopes:
|
|
7
|
+
- k8s:pods:write
|
|
8
|
+
side_effects: destructive
|
|
9
|
+
idempotent: false
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
namespace:
|
|
14
|
+
type: string
|
|
15
|
+
description: Kubernetes namespace (DNS-1123)
|
|
16
|
+
pod:
|
|
17
|
+
type: string
|
|
18
|
+
description: Pod name to delete (DNS-1123)
|
|
19
|
+
dry_run:
|
|
20
|
+
type: boolean
|
|
21
|
+
description: Validate server-side without deleting (kubectl --dry-run=server)
|
|
22
|
+
required:
|
|
23
|
+
- pod
|
|
24
|
+
output_schema:
|
|
25
|
+
type: object
|
|
26
|
+
properties:
|
|
27
|
+
deleted: { type: boolean }
|
|
28
|
+
pod: { type: string }
|
|
29
|
+
namespace: { type: string }
|
|
30
|
+
dry_run: { type: boolean }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
command: kubectl.get.pods
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: List pods in namespace (mock unless kubectl configured)
|
|
4
|
+
category: kubernetes
|
|
5
|
+
adapter: mock
|
|
6
|
+
required_scopes:
|
|
7
|
+
- k8s:pods:read
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
namespace:
|
|
14
|
+
type: string
|
|
15
|
+
default: default
|
|
16
|
+
output_schema:
|
|
17
|
+
type: object
|
|
18
|
+
properties:
|
|
19
|
+
items:
|
|
20
|
+
type: array
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
command: mcp.github.list_pulls
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: List pull requests via GitHub MCP server (governed bridge)
|
|
4
|
+
category: mcp
|
|
5
|
+
adapter: mcp
|
|
6
|
+
required_scopes:
|
|
7
|
+
- github:pull_request:read
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
mcp:
|
|
11
|
+
server_command: npx
|
|
12
|
+
server_args:
|
|
13
|
+
- "-y"
|
|
14
|
+
- "@modelcontextprotocol/server-github"
|
|
15
|
+
tool_name: list_pull_requests
|
|
16
|
+
timeout: 90
|
|
17
|
+
input_schema:
|
|
18
|
+
type: object
|
|
19
|
+
required: [repo]
|
|
20
|
+
properties:
|
|
21
|
+
repo:
|
|
22
|
+
type: string
|
|
23
|
+
description: owner/name
|
|
24
|
+
state:
|
|
25
|
+
type: string
|
|
26
|
+
enum: [open, closed, all]
|
|
27
|
+
default: open
|
|
28
|
+
output_schema:
|
|
29
|
+
type: object
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
command: pet.findpetsbystatus
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: Find pets by status
|
|
4
|
+
category: pet
|
|
5
|
+
adapter: mock
|
|
6
|
+
required_scopes:
|
|
7
|
+
- pet:read
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
status:
|
|
14
|
+
type: string
|
|
15
|
+
description: ''
|
|
16
|
+
required:
|
|
17
|
+
- status
|
|
18
|
+
output_schema:
|
|
19
|
+
type: object
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
command: pet.getpetbyid
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: Get pet by ID
|
|
4
|
+
category: pet
|
|
5
|
+
adapter: mock
|
|
6
|
+
required_scopes:
|
|
7
|
+
- pet:read
|
|
8
|
+
side_effects: read
|
|
9
|
+
idempotent: true
|
|
10
|
+
input_schema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
petId:
|
|
14
|
+
type: string
|
|
15
|
+
description: ''
|
|
16
|
+
required:
|
|
17
|
+
- petId
|
|
18
|
+
output_schema:
|
|
19
|
+
type: object
|
gax/mcp_client.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""MCP JSON-RPC over stdio — shared by gaxd MCP adapter and eval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class McpStdioClient:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
server_cmd: list[str],
|
|
16
|
+
*,
|
|
17
|
+
env: dict[str, str] | None = None,
|
|
18
|
+
timeout: float = 60.0,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._timeout = timeout
|
|
21
|
+
self._id = 0
|
|
22
|
+
proc_env = {**os.environ, **(env or {})}
|
|
23
|
+
self._proc = subprocess.Popen(
|
|
24
|
+
server_cmd,
|
|
25
|
+
stdin=subprocess.PIPE,
|
|
26
|
+
stdout=subprocess.PIPE,
|
|
27
|
+
stderr=subprocess.DEVNULL,
|
|
28
|
+
text=True,
|
|
29
|
+
env=proc_env,
|
|
30
|
+
)
|
|
31
|
+
assert self._proc.stdin and self._proc.stdout
|
|
32
|
+
self._initialize()
|
|
33
|
+
|
|
34
|
+
def _next_id(self) -> int:
|
|
35
|
+
self._id += 1
|
|
36
|
+
return self._id
|
|
37
|
+
|
|
38
|
+
def _read_message(self) -> dict[str, Any] | None:
|
|
39
|
+
line = self._proc.stdout.readline() # type: ignore[union-attr]
|
|
40
|
+
if not line:
|
|
41
|
+
return None
|
|
42
|
+
line = line.strip()
|
|
43
|
+
if not line:
|
|
44
|
+
return self._read_message()
|
|
45
|
+
return json.loads(line)
|
|
46
|
+
|
|
47
|
+
def _request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
48
|
+
rid = self._next_id()
|
|
49
|
+
req: dict[str, Any] = {"jsonrpc": "2.0", "id": rid, "method": method}
|
|
50
|
+
if params is not None:
|
|
51
|
+
req["params"] = params
|
|
52
|
+
assert self._proc.stdin
|
|
53
|
+
self._proc.stdin.write(json.dumps(req) + "\n")
|
|
54
|
+
self._proc.stdin.flush()
|
|
55
|
+
deadline = time.time() + self._timeout
|
|
56
|
+
while time.time() < deadline:
|
|
57
|
+
msg = self._read_message()
|
|
58
|
+
if msg is None:
|
|
59
|
+
break
|
|
60
|
+
if msg.get("id") == rid:
|
|
61
|
+
if "error" in msg:
|
|
62
|
+
raise RuntimeError(json.dumps(msg["error"]))
|
|
63
|
+
return msg.get("result") or {}
|
|
64
|
+
raise TimeoutError(f"MCP request timed out: {method}")
|
|
65
|
+
|
|
66
|
+
def _initialize(self) -> None:
|
|
67
|
+
self._request(
|
|
68
|
+
"initialize",
|
|
69
|
+
{
|
|
70
|
+
"protocolVersion": "2024-11-05",
|
|
71
|
+
"capabilities": {},
|
|
72
|
+
"clientInfo": {"name": "gax", "version": "0.4"},
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
assert self._proc.stdin
|
|
76
|
+
self._proc.stdin.write(
|
|
77
|
+
json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n"
|
|
78
|
+
)
|
|
79
|
+
self._proc.stdin.flush()
|
|
80
|
+
|
|
81
|
+
def list_tools(self) -> list[dict[str, Any]]:
|
|
82
|
+
result = self._request("tools/list", {})
|
|
83
|
+
return list(result.get("tools") or [])
|
|
84
|
+
|
|
85
|
+
def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
86
|
+
result = self._request("tools/call", {"name": name, "arguments": arguments})
|
|
87
|
+
content = result.get("content") or []
|
|
88
|
+
texts = [c.get("text", "") for c in content if c.get("type") == "text"]
|
|
89
|
+
if len(texts) == 1:
|
|
90
|
+
try:
|
|
91
|
+
return json.loads(texts[0])
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
return {"text": texts[0]}
|
|
94
|
+
if texts:
|
|
95
|
+
return {"texts": texts}
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
def close(self) -> None:
|
|
99
|
+
try:
|
|
100
|
+
self._proc.terminate()
|
|
101
|
+
self._proc.wait(timeout=5)
|
|
102
|
+
except Exception:
|
|
103
|
+
self._proc.kill()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def github_mcp_env() -> dict[str, str]:
|
|
107
|
+
tok = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
|
108
|
+
if not tok:
|
|
109
|
+
raise ValueError("GITHUB_TOKEN or GITHUB_PERSONAL_ACCESS_TOKEN required for MCP GitHub server")
|
|
110
|
+
return {"GITHUB_PERSONAL_ACCESS_TOKEN": tok}
|