-
Notifications
You must be signed in to change notification settings - Fork 0
122 lines (113 loc) · 5.57 KB
/
Copy pathmcp-check.yml
File metadata and controls
122 lines (113 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
name: MCP Check
on:
push:
branches: [main]
paths:
- 'plugins/**'
- 'chatgpt-app-submission.json'
- '.github/workflows/mcp-check.yml'
pull_request:
paths:
- 'plugins/**'
- 'chatgpt-app-submission.json'
- '.github/workflows/mcp-check.yml'
schedule:
- cron: '17 8 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Resolve the declared server config
run: |
set -euo pipefail
config=plugins/greptile/.mcp.json
jq -e 'if has("mcpServers") then .mcpServers else . end' "$config" > "$RUNNER_TEMP/servers.json"
if ! jq -e '.greptile | type == "object"' "$RUNNER_TEMP/servers.json" >/dev/null; then
echo "::error::$config does not declare a \"greptile\" server object in either the bare or mcpServers shape."
exit 1
fi
if ! jq -e '.greptile.url | type == "string" and startswith("https://")' "$RUNNER_TEMP/servers.json" >/dev/null; then
echo "::error::$config does not declare an https url for the greptile server."
exit 1
fi
jq -r '.greptile.url' "$RUNNER_TEMP/servers.json" > "$RUNNER_TEMP/url.txt"
echo "Resolved $(cat "$RUNNER_TEMP/url.txt")"
- name: Config uses OAuth discovery
run: |
set -euo pipefail
if jq -e '.greptile | has("headers") or has("bearer_token_env_var")' "$RUNNER_TEMP/servers.json" >/dev/null; then
echo "::error::MCP config must use OAuth discovery, without headers or bearer_token_env_var."
exit 1
fi
echo "No headers declared."
- name: Server still advertises OAuth
run: |
set -euo pipefail
url=$(cat "$RUNNER_TEMP/url.txt")
response=$(curl -sS -o /dev/null -D - -X POST "$url" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_code_reviews","arguments":{}}}' \
-w 'http_code=%{http_code}\n')
echo "$response"
if ! grep -q 'http_code=401' <<<"$response"; then
echo "::error::An unauthenticated tools/call on $url did not return 401. Codex starts the OAuth flow from that challenge."
exit 1
fi
if ! grep -qi '^www-authenticate:.*resource_metadata=' <<<"$response"; then
echo "::error::$url returned 401 without an RFC 9728 www-authenticate challenge. Codex needs resource_metadata to discover the authorization server."
exit 1
fi
echo "OAuth challenge present."
- name: README documents exactly the tools the server serves
run: |
set -euo pipefail
url=$(cat "$RUNNER_TEMP/url.txt")
curl -sS -X POST "$url" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' > "$RUNNER_TEMP/tools.json"
python3 - <<'PY'
import json, os, re, sys
with open(os.path.join(os.environ['RUNNER_TEMP'], 'tools.json')) as fh:
payload = json.load(fh)
if 'result' not in payload:
sys.exit(f"::error::tools/list returned no result: {json.dumps(payload)[:400]}")
served_tools = {tool['name']: tool for tool in payload['result']['tools']}
served = set(served_tools)
if not served:
sys.exit('::error::tools/list returned no tools.')
readme = open('plugins/greptile/README.md').read()
documented = set(re.findall(r'^- `([a-z_]+)`', readme, re.M))
documented |= set(re.findall(r'/ `([a-z_]+)`', readme))
missing = sorted(served - documented)
extra = sorted(documented - served)
if missing:
print(f"::error::README does not document: {', '.join(missing)}")
if extra:
print(f"::error::README documents tools the server does not serve: {', '.join(extra)}")
if missing or extra:
sys.exit(1)
print(f"README documents all {len(served)} served tools.")
submission = json.load(open('chatgpt-app-submission.json'))
assert set(submission['tools']) == served, 'Submission tool names differ from tools/list'
assert len(submission['test_cases']) == 5
assert len(submission['negative_test_cases']) == 3
for name, tool in submission['tools'].items():
for hint in ('readOnlyHint', 'openWorldHint', 'destructiveHint'):
assert type(tool['annotations'][hint]) is bool
assert served_tools[name].get('annotations', {}).get(hint) == tool['annotations'][hint], (
f'{name}.{hint} differs from production; deploy matching server metadata before submitting'
)
for field in ('read_only_justification', 'open_world_justification', 'destructive_justification'):
assert tool['justifications'][field].strip()
for case in submission['test_cases']:
assert set(map(str.strip, case['tools_triggered'].split(','))) <= served
assert all(case['tools_triggered'] is None for case in submission['negative_test_cases'])
print('Submission has explicit annotations, justifications, five positive cases, and three negative cases.')
PY