@synapsor/runner 0.1.0-alpha.1 → 0.1.0-alpha.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 +387 -19
- package/TRADEMARKS.md +23 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -8723
- package/dist/runner.mjs +12759 -0
- package/docs/README.md +36 -0
- package/docs/getting-started-own-database.md +460 -0
- package/docs/http-mcp.md +242 -0
- package/docs/limitations.md +95 -0
- package/docs/local-mode.md +351 -0
- package/docs/mcp-audit.md +152 -0
- package/docs/mcp-client-setup.md +231 -0
- package/docs/recipes.md +61 -0
- package/docs/release-notes.md +129 -0
- package/docs/security-boundary.md +94 -0
- package/docs/troubleshooting-first-run.md +248 -0
- package/docs/writeback-executors.md +209 -0
- package/examples/app-owned-writeback/README.md +120 -0
- package/examples/app-owned-writeback/business-actions.md +221 -0
- package/examples/app-owned-writeback/command-handler.mjs +46 -0
- package/examples/app-owned-writeback/node-fastify-handler.mjs +55 -0
- package/examples/app-owned-writeback/python-fastapi-handler.py +57 -0
- package/examples/dangerous-mcp-tools.json +88 -0
- package/examples/openai-agents-http/README.md +56 -0
- package/examples/openai-agents-http/agent.py +54 -0
- package/examples/openai-agents-http/requirements.txt +1 -0
- package/examples/openai-agents-stdio/README.md +62 -0
- package/examples/openai-agents-stdio/agent.py +70 -0
- package/examples/openai-agents-stdio/requirements.txt +1 -0
- package/examples/reference-support-billing-app/README.md +137 -0
- package/examples/reference-support-billing-app/docker-compose.yml +13 -0
- package/examples/reference-support-billing-app/mcp-client.generic.json +11 -0
- package/examples/reference-support-billing-app/schema.sql +68 -0
- package/examples/reference-support-billing-app/scripts/run-demo.sh +7 -0
- package/examples/reference-support-billing-app/seed.sql +33 -0
- package/examples/reference-support-billing-app/synapsor.runner.json +241 -0
- package/package.json +12 -4
- package/recipes/accounts.trial_extension.json +42 -0
- package/recipes/billing.late_fee_waiver.json +46 -0
- package/recipes/credits.account_credit.json +45 -0
- package/recipes/orders.refund_review.json +57 -0
- package/recipes/support.ticket_resolution.json +51 -0
- package/dist/bin.cjs +0 -13
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# App-Owned Business Action Examples
|
|
2
|
+
|
|
3
|
+
These examples show the kinds of approved proposals an app-owned handler can
|
|
4
|
+
apply. They are intentionally not direct SQL examples. Your application service
|
|
5
|
+
owns the write transaction, re-checks authorization and row/version guards, and
|
|
6
|
+
returns a terminal receipt to Synapsor Runner.
|
|
7
|
+
|
|
8
|
+
Each request uses the same boundary:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
model-facing MCP tool -> proposal
|
|
12
|
+
human/operator approval -> app-owned handler
|
|
13
|
+
handler transaction -> applied/conflict/failed receipt
|
|
14
|
+
local replay
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The handler must not trust request fields blindly. Re-check tenant,
|
|
18
|
+
principal/role, idempotency, row versions, and business policy before mutating
|
|
19
|
+
state.
|
|
20
|
+
|
|
21
|
+
## Create A Refund Review
|
|
22
|
+
|
|
23
|
+
Use this when an agent may request a refund review, but your application must
|
|
24
|
+
create the review record through normal business logic.
|
|
25
|
+
|
|
26
|
+
Capability:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
refunds.propose_refund_review(order_id, reason, requested_amount_cents)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Handler request:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"schema_version": "synapsor.handler-writeback.v1",
|
|
37
|
+
"writeback_job_id": "hwb_wrp_refund_001",
|
|
38
|
+
"proposal_id": "wrp_refund_001",
|
|
39
|
+
"idempotency_key": "wrp_refund_001",
|
|
40
|
+
"change_set": {
|
|
41
|
+
"action": "refunds.propose_refund_review",
|
|
42
|
+
"scope": {
|
|
43
|
+
"tenant_id": "acme",
|
|
44
|
+
"principal": "support_lead@example.com",
|
|
45
|
+
"object_type": "order",
|
|
46
|
+
"object_id": "ORD-3001"
|
|
47
|
+
},
|
|
48
|
+
"before": {
|
|
49
|
+
"order_status": "delivered",
|
|
50
|
+
"refund_review_id": null
|
|
51
|
+
},
|
|
52
|
+
"patch": {
|
|
53
|
+
"requested_amount_cents": 2500,
|
|
54
|
+
"reason": "duplicate charge"
|
|
55
|
+
},
|
|
56
|
+
"after": {
|
|
57
|
+
"refund_review_status": "pending_review"
|
|
58
|
+
},
|
|
59
|
+
"guards": {
|
|
60
|
+
"expected_version": {
|
|
61
|
+
"column": "updated_at",
|
|
62
|
+
"value": "2026-06-20T14:31:08Z"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"evidence": {
|
|
66
|
+
"bundle_id": "ev_refund_001"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"executor": "app_writeback_api",
|
|
70
|
+
"dry_run": false
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Handler transaction sketch:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
BEGIN
|
|
78
|
+
verify principal can request refunds for tenant acme
|
|
79
|
+
verify order ORD-3001 still belongs to tenant acme
|
|
80
|
+
verify order.updated_at still matches expected_version
|
|
81
|
+
verify no receipt exists for idempotency_key
|
|
82
|
+
INSERT INTO refund_reviews (...)
|
|
83
|
+
INSERT INTO synapsor_app_receipts (...)
|
|
84
|
+
COMMIT
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Receipt:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"status": "applied",
|
|
92
|
+
"rows_affected": 1,
|
|
93
|
+
"new_object_id": "RR-9001",
|
|
94
|
+
"source_database_mutated": true
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Insert An Account Credit Row
|
|
99
|
+
|
|
100
|
+
Use this when the safe write is an append-only ledger/accounting operation.
|
|
101
|
+
|
|
102
|
+
Capability:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
credits.propose_account_credit(customer_id, amount_cents, reason)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Handler transaction sketch:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
BEGIN
|
|
112
|
+
verify customer belongs to trusted tenant
|
|
113
|
+
verify amount is within app policy
|
|
114
|
+
verify idempotency_key has not already created a credit
|
|
115
|
+
INSERT INTO account_credits (...)
|
|
116
|
+
UPDATE customers SET credit_balance_cents = credit_balance_cents + amount
|
|
117
|
+
INSERT INTO synapsor_app_receipts (...)
|
|
118
|
+
COMMIT
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Conflict receipt if the app policy no longer allows the credit:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"status": "conflict",
|
|
126
|
+
"safe_error_code": "CREDIT_POLICY_CHANGED",
|
|
127
|
+
"source_database_mutated": false,
|
|
128
|
+
"details": {
|
|
129
|
+
"reason": "customer credit limit changed after proposal"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Open A Support Ticket
|
|
135
|
+
|
|
136
|
+
Use this when the agent may propose opening a ticket, but ticket creation must
|
|
137
|
+
go through your helpdesk/application service.
|
|
138
|
+
|
|
139
|
+
Capability:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
support.propose_open_ticket(customer_id, subject, body)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Handler transaction sketch:
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
BEGIN
|
|
149
|
+
verify principal can open support tickets for tenant
|
|
150
|
+
verify customer belongs to tenant
|
|
151
|
+
validate subject/body against app policy
|
|
152
|
+
INSERT INTO support_tickets (...)
|
|
153
|
+
INSERT INTO ticket_events (...)
|
|
154
|
+
INSERT INTO synapsor_app_receipts (...)
|
|
155
|
+
COMMIT
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Receipt:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"status": "applied",
|
|
163
|
+
"rows_affected": 2,
|
|
164
|
+
"new_object_id": "T-9100",
|
|
165
|
+
"source_database_mutated": true
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Update Multiple Related Rows
|
|
170
|
+
|
|
171
|
+
Use this when an approved business action spans several tables and should stay
|
|
172
|
+
inside your normal application transaction.
|
|
173
|
+
|
|
174
|
+
Capability:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
subscriptions.propose_trial_extension(customer_id, extension_days, reason)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Handler transaction sketch:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
BEGIN
|
|
184
|
+
verify tenant/principal authorization
|
|
185
|
+
SELECT subscription FOR UPDATE
|
|
186
|
+
verify expected subscription version
|
|
187
|
+
UPDATE subscriptions SET trial_ends_at = ...
|
|
188
|
+
INSERT INTO customer_events (...)
|
|
189
|
+
INSERT INTO billing_notes (...)
|
|
190
|
+
INSERT INTO synapsor_app_receipts (...)
|
|
191
|
+
COMMIT
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Receipt:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"status": "applied",
|
|
199
|
+
"rows_affected": 3,
|
|
200
|
+
"previous_version": "2026-06-20T14:31:08Z",
|
|
201
|
+
"new_version": "2026-06-20T14:34:19Z",
|
|
202
|
+
"source_database_mutated": true
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Idempotent Retry
|
|
207
|
+
|
|
208
|
+
If the same `idempotency_key` reaches your handler again after a successful
|
|
209
|
+
apply, return `already_applied` and the original receipt details rather than
|
|
210
|
+
running the transaction again.
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"status": "already_applied",
|
|
215
|
+
"rows_affected": 0,
|
|
216
|
+
"source_database_mutated": false,
|
|
217
|
+
"details": {
|
|
218
|
+
"original_receipt_id": "rct_abc123"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const chunks = [];
|
|
4
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
5
|
+
|
|
6
|
+
const request = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
7
|
+
const changeSet = request.change_set || {};
|
|
8
|
+
|
|
9
|
+
if (!request.proposal_id || !request.idempotency_key || !changeSet.scope?.tenant_id) {
|
|
10
|
+
process.stdout.write(JSON.stringify({
|
|
11
|
+
status: "failed",
|
|
12
|
+
safe_error_code: "BAD_WRITEBACK_REQUEST",
|
|
13
|
+
source_database_mutated: false,
|
|
14
|
+
}));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (request.dry_run) {
|
|
19
|
+
process.stdout.write(JSON.stringify({
|
|
20
|
+
status: "applied",
|
|
21
|
+
rows_affected: 0,
|
|
22
|
+
source_database_mutated: false,
|
|
23
|
+
details: { dry_run: true },
|
|
24
|
+
}));
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Put your app-owned command transaction here.
|
|
30
|
+
*
|
|
31
|
+
* Examples:
|
|
32
|
+
* - call an internal service;
|
|
33
|
+
* - enqueue a review job;
|
|
34
|
+
* - run an app migration-safe script that uses your normal ORM.
|
|
35
|
+
*
|
|
36
|
+
* Re-check tenant/principal authorization, idempotency, row/version guards,
|
|
37
|
+
* and business policy before mutating application state.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
process.stdout.write(JSON.stringify({
|
|
41
|
+
status: "applied",
|
|
42
|
+
rows_affected: 1,
|
|
43
|
+
previous_version: String(changeSet.guards?.expected_version?.value || ""),
|
|
44
|
+
new_version: new Date().toISOString(),
|
|
45
|
+
source_database_mutated: true,
|
|
46
|
+
}));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.PORT || 8787);
|
|
4
|
+
const expectedToken = process.env.SYNAPSOR_APP_WRITEBACK_TOKEN || "dev-handler-token";
|
|
5
|
+
|
|
6
|
+
const app = Fastify({ logger: true });
|
|
7
|
+
|
|
8
|
+
app.post("/synapsor/writeback", async (request, reply) => {
|
|
9
|
+
const auth = request.headers.authorization || "";
|
|
10
|
+
if (auth !== `Bearer ${expectedToken}`) {
|
|
11
|
+
return reply.code(401).send({ status: "failed", error_code: "UNAUTHORIZED" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const body = request.body || {};
|
|
15
|
+
const changeSet = body.change_set || {};
|
|
16
|
+
|
|
17
|
+
if (!body.proposal_id || !body.idempotency_key || !changeSet.scope?.tenant_id) {
|
|
18
|
+
return reply.code(400).send({ status: "failed", error_code: "BAD_WRITEBACK_REQUEST" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (body.dry_run) {
|
|
22
|
+
return {
|
|
23
|
+
status: "applied",
|
|
24
|
+
rows_affected: 0,
|
|
25
|
+
source_database_mutated: false,
|
|
26
|
+
details: { dry_run: true },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Put your app-owned transaction here.
|
|
32
|
+
*
|
|
33
|
+
* Examples:
|
|
34
|
+
* - insert a refund_review row;
|
|
35
|
+
* - insert an account_credit row;
|
|
36
|
+
* - open a support_ticket row;
|
|
37
|
+
* - update invoice + ledger rows together.
|
|
38
|
+
*
|
|
39
|
+
* Re-check:
|
|
40
|
+
* - tenant and principal authorization;
|
|
41
|
+
* - idempotency_key has not already been applied;
|
|
42
|
+
* - row/version guards still match;
|
|
43
|
+
* - requested business action is allowed by your app policy.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
status: "applied",
|
|
48
|
+
rows_affected: 1,
|
|
49
|
+
previous_version: String(changeSet.guards?.expected_version?.value || ""),
|
|
50
|
+
new_version: new Date().toISOString(),
|
|
51
|
+
source_database_mutated: true,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
app.listen({ host: "127.0.0.1", port });
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI, Header, HTTPException
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HandlerRequest(BaseModel):
|
|
9
|
+
schema_version: str
|
|
10
|
+
writeback_job_id: str
|
|
11
|
+
proposal_id: str
|
|
12
|
+
idempotency_key: str
|
|
13
|
+
change_set: dict
|
|
14
|
+
executor: str | None = None
|
|
15
|
+
dry_run: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
app = FastAPI()
|
|
19
|
+
EXPECTED_TOKEN = os.environ.get("SYNAPSOR_APP_WRITEBACK_TOKEN", "dev-handler-token")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.post("/synapsor/writeback")
|
|
23
|
+
def writeback(request: HandlerRequest, authorization: str | None = Header(default=None)):
|
|
24
|
+
if authorization != f"Bearer {EXPECTED_TOKEN}":
|
|
25
|
+
raise HTTPException(status_code=401, detail="UNAUTHORIZED")
|
|
26
|
+
|
|
27
|
+
scope = request.change_set.get("scope", {})
|
|
28
|
+
if not scope.get("tenant_id"):
|
|
29
|
+
raise HTTPException(status_code=400, detail="BAD_WRITEBACK_REQUEST")
|
|
30
|
+
|
|
31
|
+
if request.dry_run:
|
|
32
|
+
return {
|
|
33
|
+
"status": "applied",
|
|
34
|
+
"rows_affected": 0,
|
|
35
|
+
"source_database_mutated": False,
|
|
36
|
+
"details": {"dry_run": True},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Put your app-owned transaction here.
|
|
40
|
+
#
|
|
41
|
+
# Examples:
|
|
42
|
+
# - insert a refund_review row;
|
|
43
|
+
# - insert an account_credit row;
|
|
44
|
+
# - open a support_ticket row;
|
|
45
|
+
# - update invoice + ledger rows together.
|
|
46
|
+
#
|
|
47
|
+
# Re-check tenant/principal authorization, idempotency, row/version guards,
|
|
48
|
+
# and business policy before mutating application state.
|
|
49
|
+
|
|
50
|
+
expected = request.change_set.get("guards", {}).get("expected_version", {})
|
|
51
|
+
return {
|
|
52
|
+
"status": "applied",
|
|
53
|
+
"rows_affected": 1,
|
|
54
|
+
"previous_version": str(expected.get("value", "")),
|
|
55
|
+
"new_version": datetime.now(timezone.utc).isoformat(),
|
|
56
|
+
"source_database_mutated": True,
|
|
57
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tools": [
|
|
3
|
+
{
|
|
4
|
+
"name": "execute_sql",
|
|
5
|
+
"description": "Execute arbitrary SQL against the application database.",
|
|
6
|
+
"inputSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"sql": { "type": "string" }
|
|
10
|
+
},
|
|
11
|
+
"required": ["sql"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "run_query",
|
|
16
|
+
"description": "Run any query and return database rows.",
|
|
17
|
+
"inputSchema": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"query": { "type": "string" },
|
|
21
|
+
"table": { "type": "string" },
|
|
22
|
+
"columns": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": { "type": "string" }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["query"]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "approve_refund",
|
|
32
|
+
"description": "Approve and issue a customer refund immediately.",
|
|
33
|
+
"inputSchema": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"refund_id": { "type": "string" },
|
|
37
|
+
"tenant_id": { "type": "string" },
|
|
38
|
+
"amount_cents": { "type": "number" }
|
|
39
|
+
},
|
|
40
|
+
"required": ["refund_id", "tenant_id", "amount_cents"]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "update_customer",
|
|
45
|
+
"description": "Update a customer record directly.",
|
|
46
|
+
"inputSchema": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"customer_id": { "type": "string" },
|
|
50
|
+
"tenant_id": { "type": "string" },
|
|
51
|
+
"column": { "type": "string" },
|
|
52
|
+
"value": { "type": "string" }
|
|
53
|
+
},
|
|
54
|
+
"required": ["customer_id", "tenant_id", "column", "value"]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "delete_order",
|
|
59
|
+
"description": "Delete an order from the database.",
|
|
60
|
+
"inputSchema": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"order_id": { "type": "string" },
|
|
64
|
+
"tenant_id": { "type": "string" }
|
|
65
|
+
},
|
|
66
|
+
"required": ["order_id", "tenant_id"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "query_database",
|
|
71
|
+
"description": "Query arbitrary tables and columns from the database.",
|
|
72
|
+
"inputSchema": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"database": { "type": "string" },
|
|
76
|
+
"schema": { "type": "string" },
|
|
77
|
+
"table": { "type": "string" },
|
|
78
|
+
"columns": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"items": { "type": "string" }
|
|
81
|
+
},
|
|
82
|
+
"where": { "type": "string" }
|
|
83
|
+
},
|
|
84
|
+
"required": ["table"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# OpenAI Agents SDK + Synapsor Runner Streamable HTTP MCP
|
|
2
|
+
|
|
3
|
+
This example shows an OpenAI Agents SDK app connecting to a long-running
|
|
4
|
+
Synapsor Runner Streamable HTTP MCP server.
|
|
5
|
+
|
|
6
|
+
Use HTTP when your agent runs as an app/server and should connect to Runner
|
|
7
|
+
over a local/private network endpoint instead of launching a stdio child
|
|
8
|
+
process.
|
|
9
|
+
|
|
10
|
+
This example uses `synapsor-runner mcp serve-streamable-http`, the
|
|
11
|
+
spec-compatible HTTP MCP transport with `initialize` and session behavior. Use
|
|
12
|
+
`synapsor-runner mcp serve-http` only when you intentionally want the smaller
|
|
13
|
+
JSON-RPC bridge and an app-owned wrapper.
|
|
14
|
+
|
|
15
|
+
The model still sees a semantic action. It does not receive raw SQL, database
|
|
16
|
+
URLs, write credentials, approval tools, or commit tools.
|
|
17
|
+
|
|
18
|
+
## Terminal 1: Start Synapsor Runner HTTP MCP
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export DATABASE_URL="<postgres-or-mysql-read-url>"
|
|
22
|
+
export SYNAPSOR_TENANT_ID="acme"
|
|
23
|
+
export SYNAPSOR_PRINCIPAL="openai_agent_demo"
|
|
24
|
+
export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
|
|
25
|
+
|
|
26
|
+
npx -y -p @synapsor/runner@alpha synapsor-runner mcp serve-streamable-http \
|
|
27
|
+
--config ./synapsor.runner.json \
|
|
28
|
+
--store ./.synapsor/local.db \
|
|
29
|
+
--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Terminal 2: Run The Agent
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m venv .venv
|
|
36
|
+
. .venv/bin/activate
|
|
37
|
+
pip install -r requirements.txt
|
|
38
|
+
|
|
39
|
+
export OPENAI_API_KEY="..."
|
|
40
|
+
export SYNAPSOR_RUNNER_HTTP_URL="http://127.0.0.1:8766/mcp"
|
|
41
|
+
export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
|
|
42
|
+
export SYNAPSOR_INVOICE_ID="INV-3001"
|
|
43
|
+
|
|
44
|
+
python agent.py
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Expected behavior:
|
|
48
|
+
|
|
49
|
+
- the agent calls `billing.inspect_invoice` through Synapsor HTTP MCP;
|
|
50
|
+
- Synapsor applies trusted tenant/principal context from the server process;
|
|
51
|
+
- the response includes scoped data and evidence handles;
|
|
52
|
+
- no SQL/write/approval tool is exposed to the model;
|
|
53
|
+
- evidence/query audit are saved in the local Runner store.
|
|
54
|
+
|
|
55
|
+
For production-like deployment, keep HTTP MCP behind private networking/TLS,
|
|
56
|
+
bearer auth, and rate limits. See [HTTP MCP](../../docs/http-mcp.md).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from agents import Agent, Runner
|
|
7
|
+
from agents.mcp import MCPServerStreamableHttp
|
|
8
|
+
except ImportError as exc:
|
|
9
|
+
raise SystemExit(
|
|
10
|
+
"This example requires the OpenAI Agents SDK with Streamable HTTP MCP support. "
|
|
11
|
+
"Install with: pip install -r requirements.txt"
|
|
12
|
+
) from exc
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def main() -> None:
|
|
16
|
+
required = ["OPENAI_API_KEY", "SYNAPSOR_RUNNER_HTTP_URL", "SYNAPSOR_RUNNER_HTTP_TOKEN"]
|
|
17
|
+
missing = [name for name in required if not os.environ.get(name)]
|
|
18
|
+
if missing:
|
|
19
|
+
raise SystemExit(f"Missing required environment variables: {', '.join(missing)}")
|
|
20
|
+
|
|
21
|
+
invoice_id = os.environ.get("SYNAPSOR_INVOICE_ID", "INV-3001")
|
|
22
|
+
mcp_url = os.environ["SYNAPSOR_RUNNER_HTTP_URL"]
|
|
23
|
+
token = os.environ["SYNAPSOR_RUNNER_HTTP_TOKEN"]
|
|
24
|
+
|
|
25
|
+
async with MCPServerStreamableHttp(
|
|
26
|
+
params={
|
|
27
|
+
"url": mcp_url,
|
|
28
|
+
"headers": {"Authorization": f"Bearer {token}"},
|
|
29
|
+
"timeout": 15,
|
|
30
|
+
}
|
|
31
|
+
) as mcp_server:
|
|
32
|
+
agent = Agent(
|
|
33
|
+
name="Synapsor Streamable HTTP MCP demo agent",
|
|
34
|
+
instructions=(
|
|
35
|
+
"Use Synapsor MCP tools to inspect scoped database data. "
|
|
36
|
+
"Do not claim that you can run SQL, approve proposals, or commit writes."
|
|
37
|
+
),
|
|
38
|
+
mcp_servers=[mcp_server],
|
|
39
|
+
)
|
|
40
|
+
result = await Runner.run(
|
|
41
|
+
agent,
|
|
42
|
+
(
|
|
43
|
+
f"Inspect invoice {invoice_id} using Synapsor. "
|
|
44
|
+
"Explain what you saw and whether you have write authority."
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
print(result.final_output)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
try:
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
except KeyboardInterrupt:
|
|
54
|
+
sys.exit(130)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openai-agents>=0.1.0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# OpenAI Agents SDK + Synapsor Runner over stdio
|
|
2
|
+
|
|
3
|
+
This example shows an OpenAI Agents SDK app launching Synapsor Runner as a
|
|
4
|
+
local stdio MCP server.
|
|
5
|
+
|
|
6
|
+
Use stdio when the agent process can start the MCP server on the same machine.
|
|
7
|
+
The model sees Synapsor semantic tools such as `billing.inspect_invoice`. It
|
|
8
|
+
does not receive raw SQL, database URLs, write credentials, approval tools, or
|
|
9
|
+
commit tools.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Generate `synapsor.runner.json` first:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx -y -p @synapsor/runner@alpha synapsor-runner demo
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or connect your own staging database:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y -p @synapsor/runner@alpha synapsor-runner onboard db --from-env DATABASE_URL
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then install the Python dependencies:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
. .venv/bin/activate
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Run
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export OPENAI_API_KEY="..."
|
|
37
|
+
export DATABASE_URL="<postgres-or-mysql-read-url>"
|
|
38
|
+
export SYNAPSOR_TENANT_ID="acme"
|
|
39
|
+
export SYNAPSOR_PRINCIPAL="openai_agent_demo"
|
|
40
|
+
|
|
41
|
+
python agent.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Optional env:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export SYNAPSOR_CONFIG="./synapsor.runner.json"
|
|
48
|
+
export SYNAPSOR_STORE="./.synapsor/local.db"
|
|
49
|
+
export SYNAPSOR_TOOL="billing.inspect_invoice"
|
|
50
|
+
export SYNAPSOR_INVOICE_ID="INV-3001"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Expected behavior:
|
|
54
|
+
|
|
55
|
+
- the agent can inspect the scoped invoice through Synapsor;
|
|
56
|
+
- the agent cannot run SQL;
|
|
57
|
+
- the agent cannot approve or commit writes;
|
|
58
|
+
- evidence/query audit are saved in the local Runner store.
|
|
59
|
+
|
|
60
|
+
If your installed OpenAI Agents SDK does not expose `MCPServerStdio`, update the
|
|
61
|
+
SDK or use the HTTP example, which wraps Synapsor HTTP MCP with a small JSON-RPC
|
|
62
|
+
client.
|