@reproapp/mcp 0.0.8 → 0.0.10
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 +27 -170
- package/package.json +1 -1
- package/src/index.js +323 -38
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Repro MCP Server
|
|
2
2
|
|
|
3
|
-
Repro
|
|
3
|
+
Add Repro tools to an AI agent that supports MCP. Once connected, the agent can
|
|
4
|
+
inspect Repro sessions, requests, traces, database changes, and privacy policies
|
|
5
|
+
without you copying data out of Repro manually.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Add To Your AI Agent
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Default hosted Repro setup:
|
|
9
|
+
Use the default hosted Repro setup:
|
|
10
10
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
@@ -19,181 +19,38 @@ Default hosted Repro setup:
|
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Custom Repro deployment:
|
|
23
|
-
|
|
24
|
-
```json
|
|
25
|
-
{
|
|
26
|
-
"mcpServers": {
|
|
27
|
-
"repro": {
|
|
28
|
-
"command": "npx",
|
|
29
|
-
"args": ["-y", "@reproapp/mcp@latest"],
|
|
30
|
-
"env": {
|
|
31
|
-
"REPRO_AI_BASE_URL": "https://your-repro-ai-url/",
|
|
32
|
-
"REPRO_API_BASE_URL": "https://your-repro-api-url/api/",
|
|
33
|
-
"REPRO_WEB_BASE_URL": "https://your-repro-web-url/"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
22
|
The installed binary is `repro-mcp`.
|
|
41
23
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
When a user asks about Repro sessions, requests, traces, privacy policies, or app configuration, the AI client must use the Repro MCP tools only. It must not write local scripts, call Repro HTTP APIs directly, read cached Repro auth files, or patch policy JSON outside the MCP tool surface.
|
|
45
|
-
|
|
46
|
-
Privacy policy updates are intentionally handle-based:
|
|
47
|
-
|
|
48
|
-
- Use `inspect_privacy_policy` or `list_privacy_policy_blockers` first.
|
|
49
|
-
- For one policy statement, pass the exact `statementSid` to `update_privacy_policy`.
|
|
50
|
-
- For all currently listed blockers, pass the returned `blockerSetId` to `update_privacy_policy`.
|
|
51
|
-
- Broad natural-language updates such as "make all blockers keep exact" are read-only/dry-run unless they include a `blockerSetId` or exact statement ids.
|
|
52
|
-
- `KEEP_EXACT` on sensitive or raw-text review statements requires `force=true` and a human reason.
|
|
53
|
-
|
|
54
|
-
## Configuration
|
|
55
|
-
|
|
56
|
-
Built-in defaults:
|
|
57
|
-
|
|
58
|
-
- `REPRO_AI_BASE_URL=https://repro-ai-u8lhw.ondigitalocean.app`
|
|
59
|
-
- `REPRO_API_BASE_URL=https://repro-api-d7288.ondigitalocean.app/api`
|
|
60
|
-
- `REPRO_WEB_BASE_URL=https://getrepro.io`
|
|
61
|
-
|
|
62
|
-
Optional:
|
|
63
|
-
|
|
64
|
-
- `REPRO_MCP_TRANSPORT` (`stdio` or `http`; default `http` when `PORT` is set, otherwise `stdio`)
|
|
65
|
-
- `REPRO_MCP_HTTP_HOST`
|
|
66
|
-
- `REPRO_MCP_HTTP_PORT`
|
|
67
|
-
- `REPRO_MCP_HTTP_PATH`
|
|
68
|
-
- `REPRO_MCP_CORS_ORIGIN`
|
|
69
|
-
- `REPRO_API_BASE_URL`
|
|
70
|
-
- `REPRO_API_PUBLIC_BASE_URL`
|
|
71
|
-
- `REPRO_WEB_BASE_URL`
|
|
72
|
-
- `REPRO_AI_TOKEN`
|
|
73
|
-
- `REPRO_API_TOKEN`
|
|
74
|
-
- `REPRO_DEFAULT_TENANT_ID`
|
|
75
|
-
- `REPRO_APP_ID`
|
|
76
|
-
- `REPRO_REQUEST_TIMEOUT_MS`
|
|
77
|
-
- `REPRO_INVESTIGATE_TIMEOUT_MS`
|
|
78
|
-
- `REPRO_MCP_ENV_FILE`
|
|
79
|
-
- `REPRO_MCP_AUTH_FILE`
|
|
80
|
-
- `REPRO_MCP_AUTH_TIMEOUT_MS`
|
|
81
|
-
- `REPRO_MCP_AUTO_AUTH`
|
|
82
|
-
- `REPRO_MCP_AUTH_MODE` (`wait` or `prompt`; default `prompt` for HTTP and `wait` for stdio)
|
|
83
|
-
- `REPRO_MCP_OPEN_BROWSER`
|
|
84
|
-
- `REPRO_MCP_DEBUG_LOG`
|
|
85
|
-
- `REPRO_MCP_LAUNCH_LOG`
|
|
86
|
-
- `REPRO_PATH_MAPPINGS`
|
|
87
|
-
|
|
88
|
-
Hosted HTTP example:
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
export REPRO_MCP_HTTP_HOST="0.0.0.0"
|
|
92
|
-
export REPRO_MCP_HTTP_PORT="8080"
|
|
93
|
-
export REPRO_MCP_HTTP_PATH="/mcp"
|
|
94
|
-
export REPRO_AI_BASE_URL="http://repro-ai:4011/"
|
|
95
|
-
export REPRO_API_BASE_URL="http://api:4010/api/"
|
|
96
|
-
export REPRO_API_PUBLIC_BASE_URL="https://api.example.com/api/"
|
|
97
|
-
export REPRO_WEB_BASE_URL="https://app.example.com/"
|
|
98
|
-
export REPRO_DEFAULT_TENANT_ID="TENANT_123"
|
|
99
|
-
export REPRO_INVESTIGATE_TIMEOUT_MS="90000"
|
|
100
|
-
export REPRO_MCP_AUTO_AUTH="true"
|
|
101
|
-
export REPRO_MCP_AUTH_MODE="prompt"
|
|
102
|
-
export REPRO_MCP_OPEN_BROWSER="false"
|
|
103
|
-
export REPRO_PATH_MAPPINGS='[
|
|
104
|
-
{
|
|
105
|
-
"remotePrefix": "/srv/repro-demo",
|
|
106
|
-
"localPrefix": "/Users/alice/work/repro-demo"
|
|
107
|
-
}
|
|
108
|
-
]'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
`run-repro-mcp.sh` loads `repro-mcp/.env`, then `repro-mcp/.env.local`, then the file pointed to by `REPRO_MCP_ENV_FILE` when provided. Environment variables already set by the process take precedence in `src/index.js`; for DigitalOcean App Platform, prefer setting values in the app's environment configuration instead of committing an env file.
|
|
112
|
-
|
|
113
|
-
`REPRO_API_BASE_URL` is used for privacy policy inspection/update tools and browser authentication. `REPRO_WEB_BASE_URL` is used for Privacy Studio links.
|
|
114
|
-
|
|
115
|
-
For hosted MCP, point `REPRO_API_BASE_URL` at the private/internal API address used by the MCP container. Set `REPRO_API_PUBLIC_BASE_URL` to the public API origin users can open in a browser. The MCP server sends that public origin to the API when it creates browser-auth sessions, so the returned authorize URL is browser-accessible even when MCP talks to the API over VPC.
|
|
116
|
-
|
|
117
|
-
`REPRO_PATH_MAPPINGS` is optional. Use it only when telemetry contains deployed code paths that should map to a checkout available inside the MCP runtime. For a hosted container without a mounted checkout, keep it as `[]`.
|
|
118
|
-
|
|
119
|
-
For local development, copy `.env.local.example` to `.env.local`. It contains the old localhost defaults:
|
|
120
|
-
|
|
121
|
-
- `REPRO_AI_BASE_URL=http://127.0.0.1:4011`
|
|
122
|
-
- `REPRO_API_BASE_URL=http://127.0.0.1:4010/api`
|
|
123
|
-
- `REPRO_WEB_BASE_URL=http://127.0.0.1:3000`
|
|
124
|
-
- local `REPRO_PATH_MAPPINGS` for the demo checkout
|
|
125
|
-
|
|
126
|
-
If `REPRO_AI_TOKEN` and `REPRO_API_TOKEN` are not set, `repro-mcp` starts a browser login flow against `REPRO_API_BASE_URL`, caches the resulting app-user bearer token, and reuses it on future requests. Stdio uses `REPRO_MCP_AUTH_MODE=wait`: it prints the authorize URL and waits for approval. HTTP uses `REPRO_MCP_AUTH_MODE=prompt`: the first protected MCP request returns a JSON-RPC error with `error.data.authorizeUrl`; open that URL, approve access, then retry the same MCP request.
|
|
127
|
-
|
|
128
|
-
For the cached app-user token to work with `repro-ai`, configure `repro-ai` with the same `APP_USER_JWT_SECRET` as `repro-api`. Static `REPRO_AI_TOKEN` / `REPRO_API_TOKEN` values are optional overrides, not the normal hosted browser-auth path.
|
|
24
|
+
## Use It
|
|
129
25
|
|
|
130
|
-
|
|
26
|
+
After adding the MCP server, ask your AI agent Repro-specific questions:
|
|
131
27
|
|
|
132
|
-
|
|
28
|
+
```text
|
|
29
|
+
Investigate why session SESSION_123 failed checkout.
|
|
133
30
|
|
|
134
|
-
|
|
31
|
+
Show the request, trace, and DB evidence for this bug report.
|
|
135
32
|
|
|
136
|
-
|
|
137
|
-
docker build -t repro-mcp ./repro-mcp
|
|
138
|
-
```
|
|
33
|
+
Inspect privacy policy blockers for APP_123, serviceName orders-api, in dev.
|
|
139
34
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
REPRO_MCP_HTTP_HOST=0.0.0.0
|
|
144
|
-
PORT=8080
|
|
145
|
-
REPRO_MCP_HTTP_PATH=/mcp
|
|
146
|
-
REPRO_AI_BASE_URL=http://repro-ai:4011/
|
|
147
|
-
REPRO_API_BASE_URL=http://api:4010/api/
|
|
148
|
-
REPRO_API_PUBLIC_BASE_URL=https://your-public-api-host/api/
|
|
149
|
-
REPRO_WEB_BASE_URL=https://your-repro-web-host/
|
|
150
|
-
REPRO_MCP_AUTO_AUTH=true
|
|
151
|
-
REPRO_MCP_AUTH_MODE=prompt
|
|
152
|
-
REPRO_MCP_OPEN_BROWSER=false
|
|
153
|
-
REPRO_PATH_MAPPINGS=[]
|
|
35
|
+
Update these privacy blockers using blockerSetId BLOCKERS_123, then explain what still needs review.
|
|
154
36
|
```
|
|
155
37
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- `repro-mcp` runs as a Web Service from `repro-mcp/Dockerfile` on port `8080`.
|
|
159
|
-
- `repro-mcp` reaches `repro-api` and `repro-ai` through VPC/internal routing.
|
|
160
|
-
- `REPRO_API_PUBLIC_BASE_URL` points to the public API URL used only for the browser authorize page.
|
|
161
|
-
- `repro-api` and `repro-ai` share the same `APP_USER_JWT_SECRET`, so the browser-auth app-user JWT can authorize both services.
|
|
162
|
-
|
|
163
|
-
HTTP endpoints:
|
|
164
|
-
|
|
165
|
-
- `GET /health` and `GET /healthz`: health checks
|
|
166
|
-
- `POST /mcp`: JSON-RPC MCP requests
|
|
167
|
-
- `GET /mcp`: event-stream placeholder for clients that open a stream
|
|
38
|
+
If authentication is required, the agent will provide an authorization URL.
|
|
39
|
+
Open it, approve access, then retry the same request.
|
|
168
40
|
|
|
169
|
-
|
|
41
|
+
## Privacy Policy Safety
|
|
170
42
|
|
|
171
|
-
|
|
43
|
+
Privacy policy updates are handle-based:
|
|
172
44
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
45
|
+
- Pass `serviceName` when an app has separate runtime policies per backend service.
|
|
46
|
+
- Inspect first with `inspect_privacy_policy` or `list_privacy_policy_blockers`.
|
|
47
|
+
- Update one statement with its exact `statementSid`.
|
|
48
|
+
- Update a listed blocker set with the exact `blockerSetId`.
|
|
49
|
+
- Broad requests such as "make all blockers keep exact" are read-only unless they include exact ids.
|
|
50
|
+
- Sensitive `KEEP_EXACT` decisions require an explicit human reason.
|
|
178
51
|
|
|
179
|
-
|
|
180
|
-
npm run start:stdio
|
|
181
|
-
```
|
|
52
|
+
## Agent Rules
|
|
182
53
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- `resolve_scope`
|
|
187
|
-
- `list_actions`
|
|
188
|
-
- `list_requests`
|
|
189
|
-
- `get_request`
|
|
190
|
-
- `get_captured_value`
|
|
191
|
-
- `get_requests_relative_to_anchor`
|
|
192
|
-
- `get_db_changes`
|
|
193
|
-
- `get_trace`
|
|
194
|
-
- `get_action_path`
|
|
195
|
-
- `get_function_candidates`
|
|
196
|
-
- `inspect_privacy_policy`
|
|
197
|
-
- `list_privacy_policy_blockers`
|
|
198
|
-
- `update_privacy_policy`
|
|
199
|
-
- `approve_privacy_policy`
|
|
54
|
+
For Repro-scoped questions about sessions, requests, traces, privacy policies,
|
|
55
|
+
apps, or configuration, use the Repro MCP tools. Do not call Repro HTTP APIs
|
|
56
|
+
directly or edit policy JSON outside Repro.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -16,6 +16,8 @@ const SERVER_INSTRUCTIONS = [
|
|
|
16
16
|
'For Repro-scoped questions about sessions, requests, traces, privacy policies, apps, or configuration, use the Repro MCP tools only.',
|
|
17
17
|
'Do not create local scripts, read cached Repro auth files, or call Repro HTTP APIs directly for Repro data or policy workflows.',
|
|
18
18
|
'For privacy policy writes, inspect first and then use exact statementSid(s) or blockerSetId. Broad natural-language policy update queries are read-only.',
|
|
19
|
+
'For factual debugging questions, use exact evidence tools first and answer from their structured output. Use investigate only after exact evidence is gathered, or when the user explicitly asks for synthesis/root cause.',
|
|
20
|
+
'Never answer that captured client data is absent because code appears to omit it. Verify with request, DB, trace, function-call, and message payload tools, then say exactly what Repro captured or did not capture.',
|
|
19
21
|
].join(' ');
|
|
20
22
|
const DEFAULT_REPRO_AI_BASE_URL = 'https://repro-ai-u8lhw.ondigitalocean.app';
|
|
21
23
|
const DEFAULT_REPRO_API_BASE_URL = 'https://repro-api-d7288.ondigitalocean.app/api';
|
|
@@ -36,6 +38,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
36
38
|
properties: {
|
|
37
39
|
tenantId: { type: 'string' },
|
|
38
40
|
appId: { type: 'string' },
|
|
41
|
+
serviceName: { type: 'string' },
|
|
39
42
|
sessionId: { type: 'string' },
|
|
40
43
|
environment: { type: 'string' },
|
|
41
44
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
@@ -54,6 +57,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
54
57
|
properties: {
|
|
55
58
|
tenantId: { type: 'string' },
|
|
56
59
|
appId: { type: 'string' },
|
|
60
|
+
serviceName: { type: 'string' },
|
|
57
61
|
appIds: {
|
|
58
62
|
type: 'array',
|
|
59
63
|
items: { type: 'string' },
|
|
@@ -77,6 +81,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
77
81
|
properties: {
|
|
78
82
|
tenantId: { type: 'string' },
|
|
79
83
|
appId: { type: 'string' },
|
|
84
|
+
serviceName: { type: 'string' },
|
|
80
85
|
environment: { type: 'string' },
|
|
81
86
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
82
87
|
query: { type: 'string' },
|
|
@@ -99,6 +104,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
99
104
|
properties: {
|
|
100
105
|
tenantId: { type: 'string' },
|
|
101
106
|
appId: { type: 'string' },
|
|
107
|
+
serviceName: { type: 'string' },
|
|
102
108
|
environment: { type: 'string' },
|
|
103
109
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
104
110
|
query: { type: 'string' },
|
|
@@ -117,6 +123,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
117
123
|
properties: {
|
|
118
124
|
tenantId: { type: 'string' },
|
|
119
125
|
appId: { type: 'string' },
|
|
126
|
+
serviceName: { type: 'string' },
|
|
120
127
|
environment: { type: 'string' },
|
|
121
128
|
state: { type: 'string', enum: ['active', 'pending', 'approved'] },
|
|
122
129
|
query: { type: 'string' },
|
|
@@ -168,6 +175,7 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
168
175
|
properties: {
|
|
169
176
|
tenantId: { type: 'string' },
|
|
170
177
|
appId: { type: 'string' },
|
|
178
|
+
serviceName: { type: 'string' },
|
|
171
179
|
environment: { type: 'string' },
|
|
172
180
|
openUi: { type: 'boolean' },
|
|
173
181
|
},
|
|
@@ -193,6 +201,35 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
193
201
|
},
|
|
194
202
|
];
|
|
195
203
|
|
|
204
|
+
const TOOL_DESCRIPTION_OVERRIDES = {
|
|
205
|
+
investigate:
|
|
206
|
+
'Synthesize a root-cause hypothesis from Repro evidence after exact telemetry tools have been checked. Do not use this as the first or only tool for factual inventory questions about requests, DB changes, traces, function args/returns, emails, Kafka/RabbitMQ/messages, or payload contents; call the exact evidence tool first and cite that output.',
|
|
207
|
+
list_requests:
|
|
208
|
+
'Exact evidence tool: list captured HTTP and backend transport requests in scope. Use this before investigate whenever the user asks what request happened, which endpoint ran, request timing/status, or which request RID anchors later DB/trace/message evidence.',
|
|
209
|
+
get_request:
|
|
210
|
+
'Exact evidence tool: fetch one captured request by RID, including request/response bodies and captured value refs when present. Use this before answering factual questions about a specific request payload, response, endpoint, or transport message wrapper.',
|
|
211
|
+
list_session_evidence:
|
|
212
|
+
'Generic exact evidence tool: list every supported kind of captured session evidence without client-specific assumptions. Use this first for factual inventory questions about emails, messages, requests, DB changes, traces, actions, payloads, or whether data exists.',
|
|
213
|
+
get_session_evidence:
|
|
214
|
+
'Generic exact evidence tool: fetch the full structured data for one item returned by list_session_evidence. Use this before claiming captured fields are absent or omitted.',
|
|
215
|
+
get_requests_relative_to_anchor:
|
|
216
|
+
'Exact evidence tool: list captured requests before or after an action/request/DB anchor. Use for ordering questions and localizing the request sequence around a UI action or backend event.',
|
|
217
|
+
get_db_changes:
|
|
218
|
+
'Exact evidence tool: fetch captured DB/Redis changes. Use this before answering what collections, documents, fields, queries, mutations, or result metadata changed. Set includeQueryDetails when the user needs query/update/body details.',
|
|
219
|
+
get_trace:
|
|
220
|
+
'Exact evidence tool: fetch captured trace batches for a request RID. Use this before answering which functions ran, trace timing, thrown errors, or trace event details for a request.',
|
|
221
|
+
get_action_path:
|
|
222
|
+
'Exact evidence tool: fetch the correlated action path with requests, DB changes, and emails/messages when available. Use this to connect a UI action to backend evidence before giving a narrative answer.',
|
|
223
|
+
get_function_candidates:
|
|
224
|
+
'Exact evidence tool: find likely captured functions for an action/request/function search. Use this before get_function_calls when the exact function anchor is unclear.',
|
|
225
|
+
get_function_calls:
|
|
226
|
+
'Exact evidence tool: fetch concrete captured function calls, arguments, return values, errors, and value refs. Use this before answering what function handled something or what args/returns were captured.',
|
|
227
|
+
get_kafka_messages:
|
|
228
|
+
'Exact evidence tool: fetch captured Kafka publish/consume payloads. Use this for messaging questions and payload contents; for exact single-message lookups, the MCP server expands captured large values automatically.',
|
|
229
|
+
get_captured_value:
|
|
230
|
+
'Exact evidence tool: fetch the full masked value behind a captured valueId. Use this whenever another tool reports a captured value reference or large_value_available before claiming the payload is unavailable.',
|
|
231
|
+
};
|
|
232
|
+
|
|
196
233
|
function loadConfig(env = process.env) {
|
|
197
234
|
const baseUrl = normalizeBaseUrl(env.REPRO_AI_BASE_URL || DEFAULT_REPRO_AI_BASE_URL);
|
|
198
235
|
const apiBaseUrl = normalizeOptionalBaseUrl(env.REPRO_API_BASE_URL, DEFAULT_REPRO_API_BASE_URL);
|
|
@@ -1141,11 +1178,20 @@ async function handleRpcMessage(message, config, client, getTools) {
|
|
|
1141
1178
|
function publicToolDefinition(tool) {
|
|
1142
1179
|
return {
|
|
1143
1180
|
name: tool.name,
|
|
1144
|
-
description: tool
|
|
1181
|
+
description: publicToolDescription(tool),
|
|
1145
1182
|
inputSchema: tool.inputSchema,
|
|
1146
1183
|
};
|
|
1147
1184
|
}
|
|
1148
1185
|
|
|
1186
|
+
function publicToolDescription(tool) {
|
|
1187
|
+
const base = typeof tool?.description === 'string' ? tool.description.trim() : '';
|
|
1188
|
+
const override = TOOL_DESCRIPTION_OVERRIDES[tool?.name];
|
|
1189
|
+
if (!override) {
|
|
1190
|
+
return base;
|
|
1191
|
+
}
|
|
1192
|
+
return base ? `${override} ${base}` : override;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1149
1195
|
function normalizeToolCatalog(catalog) {
|
|
1150
1196
|
if (!catalog || typeof catalog !== 'object' || !Array.isArray(catalog.tools)) {
|
|
1151
1197
|
throw new Error('repro-ai returned an invalid tool catalog');
|
|
@@ -1226,7 +1272,7 @@ function inputWithExactCapturedPayloads(name, input) {
|
|
|
1226
1272
|
}
|
|
1227
1273
|
|
|
1228
1274
|
async function expandLinkedCapturedPayloads(name, response, client, originalInput = {}) {
|
|
1229
|
-
if (name
|
|
1275
|
+
if (name === 'get_captured_value') {
|
|
1230
1276
|
return response;
|
|
1231
1277
|
}
|
|
1232
1278
|
|
|
@@ -1235,20 +1281,41 @@ async function expandLinkedCapturedPayloads(name, response, client, originalInpu
|
|
|
1235
1281
|
|
|
1236
1282
|
while (true) {
|
|
1237
1283
|
const links = collectCapturedPayloadLinks(root)
|
|
1238
|
-
.filter((entry) => !visited.has(
|
|
1239
|
-
|
|
1284
|
+
.filter((entry) => !visited.has(capturedPayloadExpansionKey(entry)));
|
|
1285
|
+
const valueRefs = collectCapturedValueRefs(root)
|
|
1286
|
+
.filter((entry) => !visited.has(capturedPayloadExpansionKey(entry)));
|
|
1287
|
+
const expansions = [...links, ...valueRefs];
|
|
1288
|
+
if (!expansions.length) {
|
|
1240
1289
|
return root;
|
|
1241
1290
|
}
|
|
1242
1291
|
|
|
1243
|
-
for (const
|
|
1244
|
-
const key =
|
|
1292
|
+
for (const expansion of expansions) {
|
|
1293
|
+
const key = capturedPayloadExpansionKey(expansion);
|
|
1245
1294
|
visited.add(key);
|
|
1246
1295
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1296
|
+
if (expansion.kind === 'value-capture') {
|
|
1297
|
+
const valueId = expansion.capture?.valueId;
|
|
1298
|
+
if (typeof valueId !== 'string' || !valueId.trim()) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const expanded = await client.executeTool('get_captured_value', {
|
|
1302
|
+
...originalInput,
|
|
1303
|
+
valueId: valueId.trim(),
|
|
1304
|
+
});
|
|
1305
|
+
const value = expanded?.data?.capturedValue?.value ?? expanded?.capturedValue?.value;
|
|
1306
|
+
if (typeof value === 'undefined') {
|
|
1307
|
+
throw new Error(`Repro MCP could not expand captured value ${valueId}`);
|
|
1308
|
+
}
|
|
1309
|
+
expansion.holder[expansion.targetKey] = value;
|
|
1310
|
+
const availabilityKey = `${expansion.targetKey}Availability`;
|
|
1311
|
+
if (typeof expansion.holder[availabilityKey] === 'string') {
|
|
1312
|
+
expansion.holder[availabilityKey] = 'captured';
|
|
1313
|
+
}
|
|
1314
|
+
continue;
|
|
1250
1315
|
}
|
|
1251
1316
|
|
|
1317
|
+
const { link } = expansion;
|
|
1318
|
+
const toolName = typeof link.tool === 'string' ? link.tool : '';
|
|
1252
1319
|
const linkArgs = link.arguments && typeof link.arguments === 'object' && !Array.isArray(link.arguments)
|
|
1253
1320
|
? link.arguments
|
|
1254
1321
|
: {};
|
|
@@ -1299,8 +1366,72 @@ function collectCapturedPayloadLinks(value, out = []) {
|
|
|
1299
1366
|
return out;
|
|
1300
1367
|
}
|
|
1301
1368
|
|
|
1302
|
-
function
|
|
1369
|
+
function collectCapturedValueRefs(value, out = []) {
|
|
1370
|
+
if (!value || typeof value !== 'object') {
|
|
1371
|
+
return out;
|
|
1372
|
+
}
|
|
1373
|
+
if (Array.isArray(value)) {
|
|
1374
|
+
for (const item of value) {
|
|
1375
|
+
collectCapturedValueRefs(item, out);
|
|
1376
|
+
}
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1381
|
+
if (
|
|
1382
|
+
key.endsWith('ValueCapture') &&
|
|
1383
|
+
nested &&
|
|
1384
|
+
typeof nested === 'object' &&
|
|
1385
|
+
!Array.isArray(nested) &&
|
|
1386
|
+
nested.status === 'stored' &&
|
|
1387
|
+
typeof nested.valueId === 'string' &&
|
|
1388
|
+
nested.valueId.trim()
|
|
1389
|
+
) {
|
|
1390
|
+
const targetKey = valueCaptureTargetKey(key);
|
|
1391
|
+
if (targetKey && shouldExpandValueCaptureTarget(value[targetKey])) {
|
|
1392
|
+
out.push({
|
|
1393
|
+
kind: 'value-capture',
|
|
1394
|
+
holder: value,
|
|
1395
|
+
captureKey: key,
|
|
1396
|
+
targetKey,
|
|
1397
|
+
capture: nested,
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
collectCapturedValueRefs(nested, out);
|
|
1402
|
+
}
|
|
1403
|
+
return out;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function shouldExpandValueCaptureTarget(value) {
|
|
1407
|
+
if (typeof value === 'undefined' || value === null) return true;
|
|
1408
|
+
return Boolean(
|
|
1409
|
+
value &&
|
|
1410
|
+
typeof value === 'object' &&
|
|
1411
|
+
!Array.isArray(value) &&
|
|
1412
|
+
value.__kind === 'omitted'
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function valueCaptureTargetKey(key) {
|
|
1417
|
+
if (key === 'responseBodyValueCapture') return 'responseBody';
|
|
1418
|
+
if (key === 'respBodyValueCapture') return 'respBody';
|
|
1419
|
+
if (!key.endsWith('ValueCapture')) return null;
|
|
1420
|
+
const base = key.slice(0, -'ValueCapture'.length);
|
|
1421
|
+
return base || null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function capturedPayloadExpansionKey(expansion) {
|
|
1425
|
+
if (expansion?.kind === 'value-capture') {
|
|
1426
|
+
return JSON.stringify({
|
|
1427
|
+
kind: 'value-capture',
|
|
1428
|
+
valueId: expansion.capture?.valueId ?? null,
|
|
1429
|
+
targetKey: expansion.targetKey ?? null,
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
const link = expansion?.link;
|
|
1303
1433
|
return JSON.stringify({
|
|
1434
|
+
kind: 'mcp-link',
|
|
1304
1435
|
tool: link?.tool ?? null,
|
|
1305
1436
|
path: link?.path ?? null,
|
|
1306
1437
|
arguments: link?.arguments ?? null,
|
|
@@ -1470,10 +1601,11 @@ async function resolveScope(input, config, client) {
|
|
|
1470
1601
|
const appId = optionalNonEmptyString(input.appId) || config.defaultAppId;
|
|
1471
1602
|
const sessionId = optionalNonEmptyString(input.sessionId);
|
|
1472
1603
|
if (appId && !sessionId) {
|
|
1604
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
1473
1605
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
1474
1606
|
const state = normalizePolicyState(input.state);
|
|
1475
1607
|
const query = optionalNonEmptyString(input.query) || '';
|
|
1476
|
-
const privacyConfig = await client.requestApi(
|
|
1608
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
1477
1609
|
method: 'GET',
|
|
1478
1610
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
1479
1611
|
});
|
|
@@ -1485,6 +1617,7 @@ async function resolveScope(input, config, client) {
|
|
|
1485
1617
|
selection?.environmentId ?? requestedEnvironment,
|
|
1486
1618
|
query,
|
|
1487
1619
|
input.openUi === true,
|
|
1620
|
+
serviceName,
|
|
1488
1621
|
);
|
|
1489
1622
|
if (ui.openRequested) {
|
|
1490
1623
|
try {
|
|
@@ -1499,6 +1632,7 @@ async function resolveScope(input, config, client) {
|
|
|
1499
1632
|
status: selection ? 'ok' : 'not_found',
|
|
1500
1633
|
kind: 'app_privacy_scope',
|
|
1501
1634
|
appId,
|
|
1635
|
+
serviceName: serviceName ?? null,
|
|
1502
1636
|
tenantId: optionalNonEmptyString(input.tenantId) || config.defaultTenantId || null,
|
|
1503
1637
|
environment: selection?.environmentId ?? requestedEnvironment ?? null,
|
|
1504
1638
|
state: selection?.state ?? state,
|
|
@@ -1559,6 +1693,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1559
1693
|
const appIds = normalizeCoverageAppIds(input, config);
|
|
1560
1694
|
const state = normalizePolicyState(input.state);
|
|
1561
1695
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
1696
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
1562
1697
|
const topic = normalizeCoverageTopic(input);
|
|
1563
1698
|
const query = queryForCoverageTopic(topic);
|
|
1564
1699
|
const includeTechnical = input.includeTechnical === true;
|
|
@@ -1566,7 +1701,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1566
1701
|
|
|
1567
1702
|
for (const appId of appIds) {
|
|
1568
1703
|
try {
|
|
1569
|
-
const privacyConfig = await client.requestApi(
|
|
1704
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
1570
1705
|
method: 'GET',
|
|
1571
1706
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
1572
1707
|
});
|
|
@@ -1578,7 +1713,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1578
1713
|
environment: requestedEnvironment ?? null,
|
|
1579
1714
|
state,
|
|
1580
1715
|
summary: 'No pending or approved privacy policy was found for this app/environment.',
|
|
1581
|
-
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false),
|
|
1716
|
+
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false, serviceName),
|
|
1582
1717
|
matches: [],
|
|
1583
1718
|
needsReviewReasons: ['No policy found.'],
|
|
1584
1719
|
});
|
|
@@ -1598,13 +1733,20 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1598
1733
|
? assessPolicyBlockerCoverage(matchedStatements, contradictions)
|
|
1599
1734
|
: assessCoverage(matchedStatements, contradictions);
|
|
1600
1735
|
const evidence = buildCoverageEvidence(matchedStatements, contradictions, { includeTechnical });
|
|
1601
|
-
const ui = buildPrivacyStudioUi(
|
|
1736
|
+
const ui = buildPrivacyStudioUi(
|
|
1737
|
+
config,
|
|
1738
|
+
appId,
|
|
1739
|
+
selection.environmentId,
|
|
1740
|
+
query,
|
|
1741
|
+
input.openUi === true,
|
|
1742
|
+
serviceName,
|
|
1743
|
+
);
|
|
1602
1744
|
const policyBlockers = buildPolicyBlockerSummaries(
|
|
1603
1745
|
blockerQuery ? matchedStatements : matchedStatements.filter(isReviewBlockerStatement),
|
|
1604
1746
|
config,
|
|
1605
1747
|
appId,
|
|
1606
1748
|
selection.environmentId,
|
|
1607
|
-
{ includeTechnical },
|
|
1749
|
+
{ includeTechnical, serviceName },
|
|
1608
1750
|
);
|
|
1609
1751
|
if (ui.openRequested) {
|
|
1610
1752
|
try {
|
|
@@ -1618,6 +1760,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1618
1760
|
|
|
1619
1761
|
results.push({
|
|
1620
1762
|
appId,
|
|
1763
|
+
serviceName: serviceName ?? null,
|
|
1621
1764
|
environment: selection.environmentId,
|
|
1622
1765
|
state: selection.state,
|
|
1623
1766
|
inspectedPolicy: {
|
|
@@ -1647,7 +1790,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1647
1790
|
environment: requestedEnvironment ?? null,
|
|
1648
1791
|
state,
|
|
1649
1792
|
summary: error instanceof Error ? error.message : String(error),
|
|
1650
|
-
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false),
|
|
1793
|
+
ui: buildPrivacyStudioUi(config, appId, requestedEnvironment, query, false, serviceName),
|
|
1651
1794
|
matches: [],
|
|
1652
1795
|
needsReviewReasons: ['Could not inspect policy because the API request failed.'],
|
|
1653
1796
|
});
|
|
@@ -1666,6 +1809,7 @@ async function checkPrivacyCoverage(input, config, client) {
|
|
|
1666
1809
|
inspected: {
|
|
1667
1810
|
requestedEnvironment: requestedEnvironment ?? null,
|
|
1668
1811
|
requestedState: state,
|
|
1812
|
+
serviceName: serviceName ?? null,
|
|
1669
1813
|
appCount: appIds.length,
|
|
1670
1814
|
},
|
|
1671
1815
|
summary: summarizeCoverageResults({ safe, needsReview, notCovered, errors }),
|
|
@@ -1989,9 +2133,10 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
1989
2133
|
throw rpcError(-32603, 'This MCP client cannot fetch app privacy policies');
|
|
1990
2134
|
}
|
|
1991
2135
|
|
|
2136
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
1992
2137
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
1993
2138
|
const state = normalizePolicyState(input.state);
|
|
1994
|
-
const privacyConfig = await client.requestApi(
|
|
2139
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
1995
2140
|
method: 'GET',
|
|
1996
2141
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
1997
2142
|
});
|
|
@@ -2000,10 +2145,18 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2000
2145
|
return {
|
|
2001
2146
|
status: 'not_found',
|
|
2002
2147
|
appId,
|
|
2148
|
+
serviceName: serviceName ?? null,
|
|
2003
2149
|
environment: requestedEnvironment ?? null,
|
|
2004
2150
|
state,
|
|
2005
2151
|
message: 'No pending or approved privacy policy exists for the requested app/environment.',
|
|
2006
|
-
ui: buildPrivacyStudioUi(
|
|
2152
|
+
ui: buildPrivacyStudioUi(
|
|
2153
|
+
config,
|
|
2154
|
+
appId,
|
|
2155
|
+
requestedEnvironment,
|
|
2156
|
+
policyInspectionQuery(input),
|
|
2157
|
+
input.openUi === true,
|
|
2158
|
+
serviceName,
|
|
2159
|
+
),
|
|
2007
2160
|
};
|
|
2008
2161
|
}
|
|
2009
2162
|
|
|
@@ -2015,7 +2168,14 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2015
2168
|
const matchedLeaves = new Set(matches.flatMap((match) => statementTargetLeaves(match.statement)));
|
|
2016
2169
|
const contradictions = findSameLeafPolicyContradictions(statements, matchedLeaves);
|
|
2017
2170
|
const uiQuery = query || contradictions[0]?.leaf || '';
|
|
2018
|
-
const ui = buildPrivacyStudioUi(
|
|
2171
|
+
const ui = buildPrivacyStudioUi(
|
|
2172
|
+
config,
|
|
2173
|
+
appId,
|
|
2174
|
+
selection.environmentId,
|
|
2175
|
+
uiQuery,
|
|
2176
|
+
input.openUi === true,
|
|
2177
|
+
serviceName,
|
|
2178
|
+
);
|
|
2019
2179
|
const blockerStatements = blockerQuery
|
|
2020
2180
|
? matches.map((match) => match.statement).filter(isReviewBlockerStatement)
|
|
2021
2181
|
: matches.map((match) => match.statement).filter(isReviewBlockerStatement);
|
|
@@ -2024,12 +2184,13 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2024
2184
|
config,
|
|
2025
2185
|
appId,
|
|
2026
2186
|
selection.environmentId,
|
|
2027
|
-
{ includeTechnical },
|
|
2187
|
+
{ includeTechnical, serviceName },
|
|
2028
2188
|
);
|
|
2029
2189
|
const blockerSet = policyBlockers.length
|
|
2030
2190
|
? rememberPrivacyBlockerSet({
|
|
2031
2191
|
tenantId: optionalNonEmptyString(input.tenantId) || config.defaultTenantId || null,
|
|
2032
2192
|
appId,
|
|
2193
|
+
serviceName,
|
|
2033
2194
|
environment: selection.environmentId,
|
|
2034
2195
|
state: selection.state,
|
|
2035
2196
|
query,
|
|
@@ -2049,6 +2210,7 @@ async function inspectPrivacyPolicy(input, config, client) {
|
|
|
2049
2210
|
return {
|
|
2050
2211
|
status: 'ok',
|
|
2051
2212
|
appId,
|
|
2213
|
+
serviceName: serviceName ?? null,
|
|
2052
2214
|
environment: selection.environmentId,
|
|
2053
2215
|
state: selection.state,
|
|
2054
2216
|
generatedAt: selection.record?.policy?.generatedAt ?? selection.record?.generatedAt ?? null,
|
|
@@ -2080,12 +2242,13 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2080
2242
|
throw rpcError(-32603, 'This MCP client cannot fetch or update app privacy policies');
|
|
2081
2243
|
}
|
|
2082
2244
|
|
|
2245
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
2083
2246
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
2084
2247
|
const state = normalizePolicyState(input.state);
|
|
2085
2248
|
const decision = normalizePolicyUpdateDecision(input.decision);
|
|
2086
2249
|
const apply = input.apply === true;
|
|
2087
2250
|
const approve = input.approve === true;
|
|
2088
|
-
const privacyConfig = await client.requestApi(
|
|
2251
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
2089
2252
|
method: 'GET',
|
|
2090
2253
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
2091
2254
|
});
|
|
@@ -2094,10 +2257,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2094
2257
|
return {
|
|
2095
2258
|
status: 'not_found',
|
|
2096
2259
|
appId,
|
|
2260
|
+
serviceName: serviceName ?? null,
|
|
2097
2261
|
environment: requestedEnvironment ?? null,
|
|
2098
2262
|
state,
|
|
2099
2263
|
summary: 'No pending or approved privacy policy exists for the requested app/environment.',
|
|
2100
|
-
ui: buildPrivacyStudioUi(
|
|
2264
|
+
ui: buildPrivacyStudioUi(
|
|
2265
|
+
config,
|
|
2266
|
+
appId,
|
|
2267
|
+
requestedEnvironment,
|
|
2268
|
+
policyInspectionQuery(input),
|
|
2269
|
+
input.openUi === true,
|
|
2270
|
+
serviceName,
|
|
2271
|
+
),
|
|
2101
2272
|
};
|
|
2102
2273
|
}
|
|
2103
2274
|
|
|
@@ -2106,6 +2277,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2106
2277
|
const blockerSetSelection = resolvePrivacyBlockerSetSelection(input, {
|
|
2107
2278
|
tenantId: optionalNonEmptyString(input.tenantId) || config.defaultTenantId || null,
|
|
2108
2279
|
appId,
|
|
2280
|
+
serviceName,
|
|
2109
2281
|
environment: selection.environmentId,
|
|
2110
2282
|
state: selection.state,
|
|
2111
2283
|
});
|
|
@@ -2114,10 +2286,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2114
2286
|
status: 'blocked',
|
|
2115
2287
|
code: blockerSetSelection.error.code,
|
|
2116
2288
|
appId,
|
|
2289
|
+
serviceName: serviceName ?? null,
|
|
2117
2290
|
environment: selection.environmentId,
|
|
2118
2291
|
state: selection.state,
|
|
2119
2292
|
summary: blockerSetSelection.error.message,
|
|
2120
|
-
ui: buildPrivacyStudioUi(
|
|
2293
|
+
ui: buildPrivacyStudioUi(
|
|
2294
|
+
config,
|
|
2295
|
+
appId,
|
|
2296
|
+
selection.environmentId,
|
|
2297
|
+
query,
|
|
2298
|
+
input.openUi === true,
|
|
2299
|
+
serviceName,
|
|
2300
|
+
),
|
|
2121
2301
|
nextStep: 'Run inspect_privacy_policy or list_privacy_policy_blockers again, then pass the returned blockerSetId.',
|
|
2122
2302
|
};
|
|
2123
2303
|
}
|
|
@@ -2132,10 +2312,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2132
2312
|
return {
|
|
2133
2313
|
status: 'not_found',
|
|
2134
2314
|
appId,
|
|
2315
|
+
serviceName: serviceName ?? null,
|
|
2135
2316
|
environment: selection.environmentId,
|
|
2136
2317
|
state: selection.state,
|
|
2137
2318
|
summary: 'No matching policy statements were found for the requested update.',
|
|
2138
|
-
ui: buildPrivacyStudioUi(
|
|
2319
|
+
ui: buildPrivacyStudioUi(
|
|
2320
|
+
config,
|
|
2321
|
+
appId,
|
|
2322
|
+
selection.environmentId,
|
|
2323
|
+
query,
|
|
2324
|
+
input.openUi === true,
|
|
2325
|
+
serviceName,
|
|
2326
|
+
),
|
|
2139
2327
|
matches: rankPolicyStatementMatches(statements, input, query).slice(0, 8).map((match) => formatPolicyMatch(match.statement, match.score, { includeTechnical: true })),
|
|
2140
2328
|
};
|
|
2141
2329
|
}
|
|
@@ -2144,10 +2332,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2144
2332
|
status: 'blocked',
|
|
2145
2333
|
code: 'STALE_BLOCKER_SET',
|
|
2146
2334
|
appId,
|
|
2335
|
+
serviceName: serviceName ?? null,
|
|
2147
2336
|
environment: selection.environmentId,
|
|
2148
2337
|
state: selection.state,
|
|
2149
2338
|
summary: `The blockerSetId references ${blockerSetSelection.statementSids.length} statement(s), but only ${selectedStatements.length} still exist in the current policy.`,
|
|
2150
|
-
ui: buildPrivacyStudioUi(
|
|
2339
|
+
ui: buildPrivacyStudioUi(
|
|
2340
|
+
config,
|
|
2341
|
+
appId,
|
|
2342
|
+
selection.environmentId,
|
|
2343
|
+
query,
|
|
2344
|
+
input.openUi === true,
|
|
2345
|
+
serviceName,
|
|
2346
|
+
),
|
|
2151
2347
|
matches: selectedStatements.map((statement) => formatPolicyMatch(statement, 0, { includeTechnical: true })),
|
|
2152
2348
|
nextStep: 'Run inspect_privacy_policy or list_privacy_policy_blockers again, then retry with the new blockerSetId.',
|
|
2153
2349
|
};
|
|
@@ -2164,10 +2360,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2164
2360
|
status: 'blocked',
|
|
2165
2361
|
code: safety.code,
|
|
2166
2362
|
appId,
|
|
2363
|
+
serviceName: serviceName ?? null,
|
|
2167
2364
|
environment: selection.environmentId,
|
|
2168
2365
|
state: selection.state,
|
|
2169
2366
|
summary: safety.summary,
|
|
2170
|
-
ui: buildPrivacyStudioUi(
|
|
2367
|
+
ui: buildPrivacyStudioUi(
|
|
2368
|
+
config,
|
|
2369
|
+
appId,
|
|
2370
|
+
selection.environmentId,
|
|
2371
|
+
query,
|
|
2372
|
+
input.openUi === true,
|
|
2373
|
+
serviceName,
|
|
2374
|
+
),
|
|
2171
2375
|
matches: selectedStatements.map((statement) => formatPolicyMatch(statement, 0, { includeTechnical: true })),
|
|
2172
2376
|
proposed: safeBuildPolicyUpdateDiff(selection.policy, selectedStatements, decision, input.rawTextHints),
|
|
2173
2377
|
safety,
|
|
@@ -2182,6 +2386,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2182
2386
|
selection.environmentId,
|
|
2183
2387
|
query || selectedStatements[0]?.when?.selector || '',
|
|
2184
2388
|
input.openUi === true,
|
|
2389
|
+
serviceName,
|
|
2185
2390
|
);
|
|
2186
2391
|
if (ui.openRequested) {
|
|
2187
2392
|
try {
|
|
@@ -2197,6 +2402,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2197
2402
|
return {
|
|
2198
2403
|
status: 'dry_run',
|
|
2199
2404
|
appId,
|
|
2405
|
+
serviceName: serviceName ?? null,
|
|
2200
2406
|
environment: selection.environmentId,
|
|
2201
2407
|
state: selection.state,
|
|
2202
2408
|
summary: `Prepared privacy policy update for ${selectedStatements.length} statement(s). No API write was made because apply=true was not set.`,
|
|
@@ -2211,9 +2417,18 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2211
2417
|
};
|
|
2212
2418
|
}
|
|
2213
2419
|
|
|
2214
|
-
const policyTarget = privacyPolicyTargetForApp(
|
|
2420
|
+
const policyTarget = privacyPolicyTargetForApp(
|
|
2421
|
+
appId,
|
|
2422
|
+
serviceName,
|
|
2423
|
+
selection.record,
|
|
2424
|
+
selection.policy,
|
|
2425
|
+
);
|
|
2215
2426
|
const pendingConfig = await client.requestApi(
|
|
2216
|
-
|
|
2427
|
+
buildPrivacyApiPath(
|
|
2428
|
+
appId,
|
|
2429
|
+
serviceName,
|
|
2430
|
+
`/environments/${encodeURIComponent(selection.environmentId)}/policy`,
|
|
2431
|
+
),
|
|
2217
2432
|
{
|
|
2218
2433
|
method: 'PUT',
|
|
2219
2434
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
@@ -2241,7 +2456,11 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2241
2456
|
let approvedConfig = null;
|
|
2242
2457
|
if (approve) {
|
|
2243
2458
|
approvedConfig = await client.requestApi(
|
|
2244
|
-
|
|
2459
|
+
buildPrivacyApiPath(
|
|
2460
|
+
appId,
|
|
2461
|
+
serviceName,
|
|
2462
|
+
`/environments/${encodeURIComponent(selection.environmentId)}/pending/approve`,
|
|
2463
|
+
),
|
|
2245
2464
|
{
|
|
2246
2465
|
method: 'POST',
|
|
2247
2466
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
@@ -2252,6 +2471,7 @@ async function updatePrivacyPolicy(input, config, client) {
|
|
|
2252
2471
|
return {
|
|
2253
2472
|
status: approve ? 'approved' : 'pending',
|
|
2254
2473
|
appId,
|
|
2474
|
+
serviceName: serviceName ?? null,
|
|
2255
2475
|
environment: selection.environmentId,
|
|
2256
2476
|
previousState: selection.state,
|
|
2257
2477
|
summary: approve
|
|
@@ -2280,8 +2500,9 @@ async function approvePrivacyPolicy(input, config, client) {
|
|
|
2280
2500
|
throw rpcError(-32603, 'This MCP client cannot approve app privacy policies');
|
|
2281
2501
|
}
|
|
2282
2502
|
|
|
2503
|
+
const serviceName = optionalNonEmptyString(input.serviceName);
|
|
2283
2504
|
const requestedEnvironment = optionalNonEmptyString(input.environment);
|
|
2284
|
-
const privacyConfig = await client.requestApi(
|
|
2505
|
+
const privacyConfig = await client.requestApi(buildPrivacyApiPath(appId, serviceName), {
|
|
2285
2506
|
method: 'GET',
|
|
2286
2507
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
2287
2508
|
});
|
|
@@ -2290,13 +2511,25 @@ async function approvePrivacyPolicy(input, config, client) {
|
|
|
2290
2511
|
return {
|
|
2291
2512
|
status: 'not_found',
|
|
2292
2513
|
appId,
|
|
2514
|
+
serviceName: serviceName ?? null,
|
|
2293
2515
|
environment: requestedEnvironment ?? null,
|
|
2294
2516
|
summary: 'No pending privacy policy exists for the requested app/environment.',
|
|
2295
|
-
ui: buildPrivacyStudioUi(
|
|
2517
|
+
ui: buildPrivacyStudioUi(
|
|
2518
|
+
config,
|
|
2519
|
+
appId,
|
|
2520
|
+
requestedEnvironment,
|
|
2521
|
+
'',
|
|
2522
|
+
input.openUi === true,
|
|
2523
|
+
serviceName,
|
|
2524
|
+
),
|
|
2296
2525
|
};
|
|
2297
2526
|
}
|
|
2298
2527
|
const approvedConfig = await client.requestApi(
|
|
2299
|
-
|
|
2528
|
+
buildPrivacyApiPath(
|
|
2529
|
+
appId,
|
|
2530
|
+
serviceName,
|
|
2531
|
+
`/environments/${encodeURIComponent(selection.environmentId)}/pending/approve`,
|
|
2532
|
+
),
|
|
2300
2533
|
{
|
|
2301
2534
|
method: 'POST',
|
|
2302
2535
|
headers: input.tenantId ? { 'x-tenant-id': input.tenantId } : undefined,
|
|
@@ -2305,9 +2538,17 @@ async function approvePrivacyPolicy(input, config, client) {
|
|
|
2305
2538
|
return {
|
|
2306
2539
|
status: 'approved',
|
|
2307
2540
|
appId,
|
|
2541
|
+
serviceName: serviceName ?? null,
|
|
2308
2542
|
environment: selection.environmentId,
|
|
2309
2543
|
summary: 'Approved pending privacy policy through the Repro API validation path.',
|
|
2310
|
-
ui: buildPrivacyStudioUi(
|
|
2544
|
+
ui: buildPrivacyStudioUi(
|
|
2545
|
+
config,
|
|
2546
|
+
appId,
|
|
2547
|
+
selection.environmentId,
|
|
2548
|
+
'',
|
|
2549
|
+
input.openUi === true,
|
|
2550
|
+
serviceName,
|
|
2551
|
+
),
|
|
2311
2552
|
api: {
|
|
2312
2553
|
approvedEnvironmentCount: Array.isArray(approvedConfig?.environments) ? approvedConfig.environments.length : undefined,
|
|
2313
2554
|
},
|
|
@@ -2415,7 +2656,15 @@ function cleanupExpiredPrivacyBlockerSets(now = Date.now()) {
|
|
|
2415
2656
|
}
|
|
2416
2657
|
}
|
|
2417
2658
|
|
|
2418
|
-
function rememberPrivacyBlockerSet({
|
|
2659
|
+
function rememberPrivacyBlockerSet({
|
|
2660
|
+
tenantId,
|
|
2661
|
+
appId,
|
|
2662
|
+
serviceName,
|
|
2663
|
+
environment,
|
|
2664
|
+
state,
|
|
2665
|
+
query,
|
|
2666
|
+
statements,
|
|
2667
|
+
}) {
|
|
2419
2668
|
cleanupExpiredPrivacyBlockerSets();
|
|
2420
2669
|
const statementSids = mergeUniqueStrings(
|
|
2421
2670
|
(statements ?? []).map((statement) => String(statement?.sid ?? '')).filter(Boolean),
|
|
@@ -2425,6 +2674,7 @@ function rememberPrivacyBlockerSet({ tenantId, appId, environment, state, query,
|
|
|
2425
2674
|
v: 1,
|
|
2426
2675
|
tenantId: tenantId || null,
|
|
2427
2676
|
appId,
|
|
2677
|
+
serviceName: serviceName || null,
|
|
2428
2678
|
environment,
|
|
2429
2679
|
state,
|
|
2430
2680
|
queryHash: crypto.createHash('sha256').update(query || '').digest('hex').slice(0, 12),
|
|
@@ -2439,6 +2689,7 @@ function rememberPrivacyBlockerSet({ tenantId, appId, environment, state, query,
|
|
|
2439
2689
|
id,
|
|
2440
2690
|
tenantId: payload.tenantId,
|
|
2441
2691
|
appId: payload.appId,
|
|
2692
|
+
serviceName: payload.serviceName,
|
|
2442
2693
|
environment: payload.environment,
|
|
2443
2694
|
state: payload.state,
|
|
2444
2695
|
query: query || null,
|
|
@@ -2483,6 +2734,9 @@ function resolvePrivacyBlockerSetSelection(input, expected) {
|
|
|
2483
2734
|
}
|
|
2484
2735
|
const mismatches = [];
|
|
2485
2736
|
if (record.appId !== expected.appId) mismatches.push(`appId ${record.appId}`);
|
|
2737
|
+
if ((record.serviceName || null) !== (expected.serviceName || null)) {
|
|
2738
|
+
mismatches.push(`serviceName ${record.serviceName || '(app-wide)'}`);
|
|
2739
|
+
}
|
|
2486
2740
|
if (record.environment !== expected.environment) mismatches.push(`environment ${record.environment}`);
|
|
2487
2741
|
if (record.state !== expected.state) mismatches.push(`state ${record.state}`);
|
|
2488
2742
|
if (record.tenantId && expected.tenantId && record.tenantId !== expected.tenantId) {
|
|
@@ -2908,15 +3162,21 @@ function mergeUniqueStrings(values) {
|
|
|
2908
3162
|
return output;
|
|
2909
3163
|
}
|
|
2910
3164
|
|
|
2911
|
-
function privacyPolicyTargetForApp(appId, record, policy) {
|
|
3165
|
+
function privacyPolicyTargetForApp(appId, serviceName, record, policy) {
|
|
2912
3166
|
const target =
|
|
2913
3167
|
record?.proposal?.target ??
|
|
2914
3168
|
policy?.sourceProposal?.target ??
|
|
2915
3169
|
{};
|
|
2916
|
-
|
|
3170
|
+
const result = {
|
|
2917
3171
|
...target,
|
|
2918
3172
|
appId,
|
|
2919
3173
|
};
|
|
3174
|
+
if (serviceName) {
|
|
3175
|
+
result.serviceName = serviceName;
|
|
3176
|
+
} else {
|
|
3177
|
+
delete result.serviceName;
|
|
3178
|
+
}
|
|
3179
|
+
return result;
|
|
2920
3180
|
}
|
|
2921
3181
|
|
|
2922
3182
|
function cloneJson(value) {
|
|
@@ -3159,7 +3419,14 @@ function buildPolicyBlockerSummaries(statements, config, appId, environment, opt
|
|
|
3159
3419
|
const descriptor = describePolicyStatement(statement);
|
|
3160
3420
|
const review = statement?.meta?.review ?? {};
|
|
3161
3421
|
const query = descriptor.codeAnchor || descriptor.label || statement?.when?.selector || '';
|
|
3162
|
-
const ui = buildPrivacyStudioUi(
|
|
3422
|
+
const ui = buildPrivacyStudioUi(
|
|
3423
|
+
config,
|
|
3424
|
+
appId,
|
|
3425
|
+
environment,
|
|
3426
|
+
query,
|
|
3427
|
+
false,
|
|
3428
|
+
options.serviceName,
|
|
3429
|
+
);
|
|
3163
3430
|
const blocker = {
|
|
3164
3431
|
label: descriptor.label,
|
|
3165
3432
|
codeAnchor: descriptor.codeAnchor,
|
|
@@ -3402,7 +3669,22 @@ function summarizeRawTextHints(policy, query) {
|
|
|
3402
3669
|
}));
|
|
3403
3670
|
}
|
|
3404
3671
|
|
|
3405
|
-
function
|
|
3672
|
+
function buildPrivacyApiPath(appId, serviceName, suffix = '') {
|
|
3673
|
+
const path = `v1/apps/${encodeURIComponent(appId)}/privacy${suffix}`;
|
|
3674
|
+
if (!serviceName) {
|
|
3675
|
+
return path;
|
|
3676
|
+
}
|
|
3677
|
+
return `${path}?serviceName=${encodeURIComponent(serviceName)}`;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
function buildPrivacyStudioUi(
|
|
3681
|
+
config,
|
|
3682
|
+
appId,
|
|
3683
|
+
environment,
|
|
3684
|
+
query,
|
|
3685
|
+
openRequested,
|
|
3686
|
+
serviceName,
|
|
3687
|
+
) {
|
|
3406
3688
|
const baseUrl = config.webBaseUrl || deriveWebBaseUrl(config.apiBaseUrl);
|
|
3407
3689
|
if (!baseUrl) {
|
|
3408
3690
|
return {
|
|
@@ -3415,6 +3697,9 @@ function buildPrivacyStudioUi(config, appId, environment, query, openRequested)
|
|
|
3415
3697
|
|
|
3416
3698
|
const url = new URL('/admin/privacy', baseUrl);
|
|
3417
3699
|
url.searchParams.set('service', appId);
|
|
3700
|
+
if (serviceName) {
|
|
3701
|
+
url.searchParams.set('serviceName', serviceName);
|
|
3702
|
+
}
|
|
3418
3703
|
if (environment) {
|
|
3419
3704
|
url.searchParams.set('env', environment);
|
|
3420
3705
|
}
|