@floomhq/floom 5.0.11 → 5.1.0
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/LICENSE +21 -21
- package/README.md +381 -381
- package/assets/floom-skill.md +32 -0
- package/bin/floom-mcp +9 -9
- package/bin/workeros-mcp +13 -13
- package/dist/cli.js +89 -10
- package/dist/cli.js.map +1 -1
- package/dist/commands/completion.js +85 -85
- package/dist/commands/connections.js +3 -3
- package/dist/commands/connections.js.map +1 -1
- package/dist/commands/contexts.js +9 -9
- package/dist/commands/contexts.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +64 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.js +5 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +67 -8
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/runs.d.ts +1 -0
- package/dist/commands/runs.js +83 -20
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/whoami.js +5 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/workers.d.ts +3 -1
- package/dist/commands/workers.js +37 -18
- package/dist/commands/workers.js.map +1 -1
- package/dist/commands/workspaces.js +9 -4
- package/dist/commands/workspaces.js.map +1 -1
- package/dist/lib/api.js +2 -2
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/cli-errors.d.ts +3 -1
- package/dist/lib/cli-errors.js +10 -2
- package/dist/lib/cli-errors.js.map +1 -1
- package/dist/lib/credentials.js +2 -2
- package/dist/lib/credentials.js.map +1 -1
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +11 -7
- package/dist/lib/output.js.map +1 -1
- package/dist/lib/telemetry.d.ts +22 -0
- package/dist/lib/telemetry.js +85 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/dist/lib/worker-authoring.js +154 -154
- package/dist/server.js +60 -7
- package/dist/server.js.map +1 -1
- package/package.json +47 -46
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { FloomApiClient } from "./api.js";
|
|
2
|
+
import { readCredentials } from "./credentials.js";
|
|
3
|
+
function telemetryDisabled() {
|
|
4
|
+
const value = (process.env.FLOOM_CLI_TELEMETRY_DISABLED || process.env.WORKEROS_CLI_TELEMETRY_DISABLED || "")
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase();
|
|
7
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
8
|
+
}
|
|
9
|
+
export function apiBaseKind(apiBase) {
|
|
10
|
+
let host = "";
|
|
11
|
+
try {
|
|
12
|
+
host = new URL(apiBase).hostname.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return "custom";
|
|
16
|
+
}
|
|
17
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
|
|
18
|
+
return "local";
|
|
19
|
+
}
|
|
20
|
+
if (host.endsWith("floom.dev") ||
|
|
21
|
+
host.endsWith("floom.ai") ||
|
|
22
|
+
host.endsWith("floom.app") ||
|
|
23
|
+
host.endsWith("workeros.com")) {
|
|
24
|
+
return "cloud";
|
|
25
|
+
}
|
|
26
|
+
return "custom";
|
|
27
|
+
}
|
|
28
|
+
async function telemetryClient() {
|
|
29
|
+
if (telemetryDisabled())
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const credentials = await readCredentials();
|
|
33
|
+
if (!credentials)
|
|
34
|
+
return null;
|
|
35
|
+
return { client: new FloomApiClient(credentials.api_base, credentials), credentials };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function emitCliCommandTelemetry(payload) {
|
|
42
|
+
try {
|
|
43
|
+
const resolved = await telemetryClient();
|
|
44
|
+
if (!resolved)
|
|
45
|
+
return;
|
|
46
|
+
await resolved.client.requestJson("POST", "/telemetry/cli-command", {
|
|
47
|
+
body: {
|
|
48
|
+
command: payload.command,
|
|
49
|
+
success: payload.success,
|
|
50
|
+
duration_ms: Math.max(0, Math.floor(payload.duration_ms)),
|
|
51
|
+
exit_code: payload.exit_code,
|
|
52
|
+
api_base_kind: apiBaseKind(resolved.credentials.api_base),
|
|
53
|
+
worker_id: payload.worker_id,
|
|
54
|
+
run_id: payload.run_id,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Analytics must never affect CLI behavior.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function emitMcpToolTelemetry(payload) {
|
|
63
|
+
try {
|
|
64
|
+
const resolved = await telemetryClient();
|
|
65
|
+
if (!resolved)
|
|
66
|
+
return;
|
|
67
|
+
await resolved.client.requestJson("POST", "/telemetry/mcp-tool", {
|
|
68
|
+
body: {
|
|
69
|
+
tool_name: payload.tool_name,
|
|
70
|
+
success: payload.success,
|
|
71
|
+
duration_ms: Math.max(0, Math.floor(payload.duration_ms)),
|
|
72
|
+
auth_method: payload.auth_method,
|
|
73
|
+
worker_id: payload.worker_id,
|
|
74
|
+
run_id: payload.run_id,
|
|
75
|
+
status_code: payload.status_code,
|
|
76
|
+
error_category: payload.error_category,
|
|
77
|
+
is_custom_tool: payload.is_custom_tool || false,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Analytics must never affect MCP tool behavior.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/lib/telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -50,57 +50,57 @@ export const WORKER_TEMPLATES = [
|
|
|
50
50
|
title: "Python Script Worker",
|
|
51
51
|
description: "Deterministic E2B worker that reads inputs.json and writes result.json.",
|
|
52
52
|
mode: "pure-script",
|
|
53
|
-
worker_yml: `schema_version: "0.3"
|
|
54
|
-
name: text-normalizer
|
|
55
|
-
title: Text Normalizer
|
|
56
|
-
description: Normalize a text input and return the normalized value.
|
|
57
|
-
version: "0.1.0"
|
|
58
|
-
entrypoint: run.py
|
|
59
|
-
trigger:
|
|
60
|
-
type: manual
|
|
61
|
-
exec:
|
|
62
|
-
mode: pure-script
|
|
63
|
-
entry: run.py
|
|
64
|
-
runtime: python311
|
|
65
|
-
runner: e2b
|
|
66
|
-
command: python run.py
|
|
67
|
-
inputs:
|
|
68
|
-
- name: text
|
|
69
|
-
label: Text
|
|
70
|
-
type: string
|
|
71
|
-
kind: scalar
|
|
72
|
-
required: true
|
|
73
|
-
outputs:
|
|
74
|
-
- name: normalized
|
|
75
|
-
label: Normalized text
|
|
76
|
-
type: string
|
|
77
|
-
kind: scalar
|
|
78
|
-
required: true
|
|
79
|
-
capabilities:
|
|
80
|
-
network:
|
|
81
|
-
egress: false
|
|
82
|
-
secrets: []
|
|
83
|
-
connections: []
|
|
53
|
+
worker_yml: `schema_version: "0.3"
|
|
54
|
+
name: text-normalizer
|
|
55
|
+
title: Text Normalizer
|
|
56
|
+
description: Normalize a text input and return the normalized value.
|
|
57
|
+
version: "0.1.0"
|
|
58
|
+
entrypoint: run.py
|
|
59
|
+
trigger:
|
|
60
|
+
type: manual
|
|
61
|
+
exec:
|
|
62
|
+
mode: pure-script
|
|
63
|
+
entry: run.py
|
|
64
|
+
runtime: python311
|
|
65
|
+
runner: e2b
|
|
66
|
+
command: python run.py
|
|
67
|
+
inputs:
|
|
68
|
+
- name: text
|
|
69
|
+
label: Text
|
|
70
|
+
type: string
|
|
71
|
+
kind: scalar
|
|
72
|
+
required: true
|
|
73
|
+
outputs:
|
|
74
|
+
- name: normalized
|
|
75
|
+
label: Normalized text
|
|
76
|
+
type: string
|
|
77
|
+
kind: scalar
|
|
78
|
+
required: true
|
|
79
|
+
capabilities:
|
|
80
|
+
network:
|
|
81
|
+
egress: false
|
|
82
|
+
secrets: []
|
|
83
|
+
connections: []
|
|
84
84
|
`,
|
|
85
|
-
run_py: `import json
|
|
86
|
-
from pathlib import Path
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def main() -> None:
|
|
90
|
-
inputs_path = Path("inputs.json")
|
|
91
|
-
inputs = json.loads(inputs_path.read_text(encoding="utf-8")) if inputs_path.exists() else {}
|
|
92
|
-
text = str(inputs.get("text", "")).strip()
|
|
93
|
-
result = {
|
|
94
|
-
"status": "success",
|
|
95
|
-
"outputs": {"normalized": " ".join(text.split())},
|
|
96
|
-
"artifacts": [],
|
|
97
|
-
"error": None,
|
|
98
|
-
}
|
|
99
|
-
Path("result.json").write_text(json.dumps(result), encoding="utf-8")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if __name__ == "__main__":
|
|
103
|
-
main()
|
|
85
|
+
run_py: `import json
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main() -> None:
|
|
90
|
+
inputs_path = Path("inputs.json")
|
|
91
|
+
inputs = json.loads(inputs_path.read_text(encoding="utf-8")) if inputs_path.exists() else {}
|
|
92
|
+
text = str(inputs.get("text", "")).strip()
|
|
93
|
+
result = {
|
|
94
|
+
"status": "success",
|
|
95
|
+
"outputs": {"normalized": " ".join(text.split())},
|
|
96
|
+
"artifacts": [],
|
|
97
|
+
"error": None,
|
|
98
|
+
}
|
|
99
|
+
Path("result.json").write_text(json.dumps(result), encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
|
104
104
|
`,
|
|
105
105
|
notes: [
|
|
106
106
|
"Use this when the job can be completed deterministically in code.",
|
|
@@ -112,54 +112,54 @@ if __name__ == "__main__":
|
|
|
112
112
|
title: "Gmail Summary Agent",
|
|
113
113
|
description: "Agent-mode worker that uses a Gmail connection and produces a markdown summary.",
|
|
114
114
|
mode: "agent",
|
|
115
|
-
worker_yml: `schema_version: "0.3"
|
|
116
|
-
name: gmail-summary-agent
|
|
117
|
-
title: Gmail Summary Agent
|
|
118
|
-
description: Summarize recent Gmail messages into a concise markdown brief.
|
|
119
|
-
version: "0.1.0"
|
|
120
|
-
entrypoint: SKILL.md
|
|
121
|
-
trigger:
|
|
122
|
-
type: manual
|
|
123
|
-
connections:
|
|
124
|
-
- app: gmail
|
|
125
|
-
allowed_tools:
|
|
126
|
-
- GMAIL_FETCH_EMAILS
|
|
127
|
-
exec:
|
|
128
|
-
mode: agent
|
|
129
|
-
entry: SKILL.md
|
|
130
|
-
runtime: python311
|
|
131
|
-
runner: e2b
|
|
132
|
-
inputs:
|
|
133
|
-
- name: query
|
|
134
|
-
label: Gmail query
|
|
135
|
-
type: string
|
|
136
|
-
kind: scalar
|
|
137
|
-
required: false
|
|
138
|
-
default: newer_than:1d
|
|
139
|
-
outputs:
|
|
140
|
-
- name: summary
|
|
141
|
-
label: Summary
|
|
142
|
-
kind: file
|
|
143
|
-
media_type: text/markdown
|
|
144
|
-
path: out/summary.md
|
|
145
|
-
required: true
|
|
146
|
-
limits:
|
|
147
|
-
max_tool_iterations: 60
|
|
148
|
-
max_output_tokens: 100000
|
|
149
|
-
max_total_tokens: 1000000
|
|
150
|
-
timeout_seconds: 300
|
|
151
|
-
capabilities:
|
|
152
|
-
network:
|
|
153
|
-
egress: true
|
|
154
|
-
secrets: []
|
|
155
|
-
connections:
|
|
156
|
-
- gmail
|
|
115
|
+
worker_yml: `schema_version: "0.3"
|
|
116
|
+
name: gmail-summary-agent
|
|
117
|
+
title: Gmail Summary Agent
|
|
118
|
+
description: Summarize recent Gmail messages into a concise markdown brief.
|
|
119
|
+
version: "0.1.0"
|
|
120
|
+
entrypoint: SKILL.md
|
|
121
|
+
trigger:
|
|
122
|
+
type: manual
|
|
123
|
+
connections:
|
|
124
|
+
- app: gmail
|
|
125
|
+
allowed_tools:
|
|
126
|
+
- GMAIL_FETCH_EMAILS
|
|
127
|
+
exec:
|
|
128
|
+
mode: agent
|
|
129
|
+
entry: SKILL.md
|
|
130
|
+
runtime: python311
|
|
131
|
+
runner: e2b
|
|
132
|
+
inputs:
|
|
133
|
+
- name: query
|
|
134
|
+
label: Gmail query
|
|
135
|
+
type: string
|
|
136
|
+
kind: scalar
|
|
137
|
+
required: false
|
|
138
|
+
default: newer_than:1d
|
|
139
|
+
outputs:
|
|
140
|
+
- name: summary
|
|
141
|
+
label: Summary
|
|
142
|
+
kind: file
|
|
143
|
+
media_type: text/markdown
|
|
144
|
+
path: out/summary.md
|
|
145
|
+
required: true
|
|
146
|
+
limits:
|
|
147
|
+
max_tool_iterations: 60
|
|
148
|
+
max_output_tokens: 100000
|
|
149
|
+
max_total_tokens: 1000000
|
|
150
|
+
timeout_seconds: 300
|
|
151
|
+
capabilities:
|
|
152
|
+
network:
|
|
153
|
+
egress: true
|
|
154
|
+
secrets: []
|
|
155
|
+
connections:
|
|
156
|
+
- gmail
|
|
157
157
|
`,
|
|
158
|
-
skill_md: `# Gmail Summary Agent
|
|
159
|
-
|
|
160
|
-
Fetch recent Gmail messages using the declared Gmail connection, summarize the important items, and write a markdown brief to out/summary.md.
|
|
161
|
-
|
|
162
|
-
Call the Gmail runtime tool once with the declared allowed tool, summarize the returned messages, then call finish_with_outputs with the output named summary. Do not ask the user for OAuth tokens or passwords.
|
|
158
|
+
skill_md: `# Gmail Summary Agent
|
|
159
|
+
|
|
160
|
+
Fetch recent Gmail messages using the declared Gmail connection, summarize the important items, and write a markdown brief to out/summary.md.
|
|
161
|
+
|
|
162
|
+
Call the Gmail runtime tool once with the declared allowed tool, summarize the returned messages, then call finish_with_outputs with the output named summary. Do not ask the user for OAuth tokens or passwords.
|
|
163
163
|
`,
|
|
164
164
|
notes: [
|
|
165
165
|
"Use agent mode when the work needs reasoning or tool calls.",
|
|
@@ -171,64 +171,64 @@ Call the Gmail runtime tool once with the declared allowed tool, summarize the r
|
|
|
171
171
|
title: "Approval Script Worker",
|
|
172
172
|
description: "Script worker that prepares a human approval payload before an external action.",
|
|
173
173
|
mode: "pure-script",
|
|
174
|
-
worker_yml: `schema_version: "0.3"
|
|
175
|
-
name: approval-script
|
|
176
|
-
title: Approval Script
|
|
177
|
-
description: Build an approval payload for a proposed outbound action.
|
|
178
|
-
version: "0.1.0"
|
|
179
|
-
entrypoint: run.py
|
|
180
|
-
trigger:
|
|
181
|
-
type: manual
|
|
182
|
-
exec:
|
|
183
|
-
mode: pure-script
|
|
184
|
-
entry: run.py
|
|
185
|
-
runtime: python311
|
|
186
|
-
runner: e2b
|
|
187
|
-
command: python run.py
|
|
188
|
-
inputs:
|
|
189
|
-
- name: message
|
|
190
|
-
label: Message
|
|
191
|
-
type: string
|
|
192
|
-
kind: scalar
|
|
193
|
-
required: true
|
|
194
|
-
outputs:
|
|
195
|
-
- name: plan
|
|
196
|
-
label: Approval plan
|
|
197
|
-
kind: file
|
|
198
|
-
media_type: text/markdown
|
|
199
|
-
path: out/plan.md
|
|
200
|
-
required: true
|
|
201
|
-
approvals:
|
|
202
|
-
required: true
|
|
203
|
-
label: Approve outbound action
|
|
204
|
-
capabilities:
|
|
205
|
-
network:
|
|
206
|
-
egress: false
|
|
207
|
-
secrets: []
|
|
208
|
-
connections: []
|
|
174
|
+
worker_yml: `schema_version: "0.3"
|
|
175
|
+
name: approval-script
|
|
176
|
+
title: Approval Script
|
|
177
|
+
description: Build an approval payload for a proposed outbound action.
|
|
178
|
+
version: "0.1.0"
|
|
179
|
+
entrypoint: run.py
|
|
180
|
+
trigger:
|
|
181
|
+
type: manual
|
|
182
|
+
exec:
|
|
183
|
+
mode: pure-script
|
|
184
|
+
entry: run.py
|
|
185
|
+
runtime: python311
|
|
186
|
+
runner: e2b
|
|
187
|
+
command: python run.py
|
|
188
|
+
inputs:
|
|
189
|
+
- name: message
|
|
190
|
+
label: Message
|
|
191
|
+
type: string
|
|
192
|
+
kind: scalar
|
|
193
|
+
required: true
|
|
194
|
+
outputs:
|
|
195
|
+
- name: plan
|
|
196
|
+
label: Approval plan
|
|
197
|
+
kind: file
|
|
198
|
+
media_type: text/markdown
|
|
199
|
+
path: out/plan.md
|
|
200
|
+
required: true
|
|
201
|
+
approvals:
|
|
202
|
+
required: true
|
|
203
|
+
label: Approve outbound action
|
|
204
|
+
capabilities:
|
|
205
|
+
network:
|
|
206
|
+
egress: false
|
|
207
|
+
secrets: []
|
|
208
|
+
connections: []
|
|
209
209
|
`,
|
|
210
|
-
run_py: `import json
|
|
211
|
-
from pathlib import Path
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def main() -> None:
|
|
215
|
-
inputs = json.loads(Path("inputs.json").read_text(encoding="utf-8"))
|
|
216
|
-
message = str(inputs.get("message", "")).strip()
|
|
217
|
-
out_dir = Path("out")
|
|
218
|
-
out_dir.mkdir(exist_ok=True)
|
|
219
|
-
plan_path = out_dir / "plan.md"
|
|
220
|
-
plan_path.write_text(f"# Proposed action\\n\\n{message}\\n", encoding="utf-8")
|
|
221
|
-
result = {
|
|
222
|
-
"status": "success",
|
|
223
|
-
"outputs": {"plan": str(plan_path)},
|
|
224
|
-
"artifacts": [{"name": "plan.md", "path": str(plan_path), "type": "text/markdown"}],
|
|
225
|
-
"error": None,
|
|
226
|
-
}
|
|
227
|
-
Path("result.json").write_text(json.dumps(result), encoding="utf-8")
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if __name__ == "__main__":
|
|
231
|
-
main()
|
|
210
|
+
run_py: `import json
|
|
211
|
+
from pathlib import Path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main() -> None:
|
|
215
|
+
inputs = json.loads(Path("inputs.json").read_text(encoding="utf-8"))
|
|
216
|
+
message = str(inputs.get("message", "")).strip()
|
|
217
|
+
out_dir = Path("out")
|
|
218
|
+
out_dir.mkdir(exist_ok=True)
|
|
219
|
+
plan_path = out_dir / "plan.md"
|
|
220
|
+
plan_path.write_text(f"# Proposed action\\n\\n{message}\\n", encoding="utf-8")
|
|
221
|
+
result = {
|
|
222
|
+
"status": "success",
|
|
223
|
+
"outputs": {"plan": str(plan_path)},
|
|
224
|
+
"artifacts": [{"name": "plan.md", "path": str(plan_path), "type": "text/markdown"}],
|
|
225
|
+
"error": None,
|
|
226
|
+
}
|
|
227
|
+
Path("result.json").write_text(json.dumps(result), encoding="utf-8")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|
|
232
232
|
`,
|
|
233
233
|
notes: [
|
|
234
234
|
"Use approvals.required when a human must review before publication or side effects.",
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { readCredentials } from "./lib/credentials.js";
|
|
9
9
|
import { WORKER_AUTHORING_CONTRACT, getWorkerTemplate, listWorkerTemplates, validateWorkerDraft, } from "./lib/worker-authoring.js";
|
|
10
|
+
import { emitMcpToolTelemetry } from "./lib/telemetry.js";
|
|
10
11
|
const DEFAULT_API_BASE = "http://localhost:8000";
|
|
11
12
|
const TERMINAL_RUN_STATUSES = new Set([
|
|
12
13
|
"success",
|
|
@@ -27,14 +28,14 @@ class FloomApiError extends Error {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
function apiBase() {
|
|
30
|
-
return (process.env.WORKEROS_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
31
|
+
return (process.env.FLOOM_API_BASE || process.env.WORKEROS_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
31
32
|
}
|
|
32
33
|
function hostedModeRequested() {
|
|
33
34
|
const value = (process.env.WORKEROS_CLOUD || "").trim().toLowerCase();
|
|
34
35
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
35
36
|
}
|
|
36
37
|
function isHostedApi() {
|
|
37
|
-
return Boolean(process.env.WORKEROS_API_TOKEN) || hostedModeRequested();
|
|
38
|
+
return Boolean(process.env.FLOOM_TOKEN || process.env.WORKEROS_API_TOKEN) || hostedModeRequested();
|
|
38
39
|
}
|
|
39
40
|
function resolvePath(path) {
|
|
40
41
|
if (!isHostedApi())
|
|
@@ -62,14 +63,14 @@ function activeWorkspaceId() {
|
|
|
62
63
|
}
|
|
63
64
|
function authHeader() {
|
|
64
65
|
const headers = {};
|
|
65
|
-
const token = process.env.WORKEROS_API_TOKEN
|
|
66
|
+
const token = (process.env.FLOOM_TOKEN || process.env.WORKEROS_API_TOKEN || "").trim();
|
|
66
67
|
if (token) {
|
|
67
68
|
headers["x-floom-token"] = token;
|
|
68
69
|
}
|
|
69
70
|
else {
|
|
70
71
|
const secret = process.env.WORKEROS_API_SECRET?.trim();
|
|
71
72
|
if (!secret) {
|
|
72
|
-
throw new Error("WORKEROS_API_TOKEN or WORKEROS_API_SECRET is required");
|
|
73
|
+
throw new Error("FLOOM_TOKEN, WORKEROS_API_TOKEN, or WORKEROS_API_SECRET is required");
|
|
73
74
|
}
|
|
74
75
|
headers["x-floom-secret"] = secret;
|
|
75
76
|
// Self-hosted engines with user-header scope require x-floom-user (OSS only).
|
|
@@ -158,6 +159,58 @@ async function callTool(handler) {
|
|
|
158
159
|
return errorResult(error);
|
|
159
160
|
}
|
|
160
161
|
}
|
|
162
|
+
function mcpTelemetryIds(result) {
|
|
163
|
+
const content = result.structuredContent && typeof result.structuredContent === "object"
|
|
164
|
+
? result.structuredContent
|
|
165
|
+
: {};
|
|
166
|
+
const workerId = typeof content.worker_id === "string"
|
|
167
|
+
? content.worker_id
|
|
168
|
+
: typeof content.id === "string" && String(content.id).startsWith("w")
|
|
169
|
+
? String(content.id)
|
|
170
|
+
: undefined;
|
|
171
|
+
const runId = typeof content.run_id === "string"
|
|
172
|
+
? content.run_id
|
|
173
|
+
: typeof content.id === "string" && String(content.id).startsWith("run")
|
|
174
|
+
? String(content.id)
|
|
175
|
+
: undefined;
|
|
176
|
+
const status = typeof content.status === "number" ? content.status : undefined;
|
|
177
|
+
return { worker_id: workerId, run_id: runId, status_code: status };
|
|
178
|
+
}
|
|
179
|
+
function installMcpTelemetry(server) {
|
|
180
|
+
const originalRegisterTool = server.registerTool.bind(server);
|
|
181
|
+
server.registerTool = (name, config, handler) => {
|
|
182
|
+
return originalRegisterTool(name, config, async (...args) => {
|
|
183
|
+
const startedAt = Date.now();
|
|
184
|
+
try {
|
|
185
|
+
const result = await handler(...args);
|
|
186
|
+
const ids = mcpTelemetryIds(result);
|
|
187
|
+
await emitMcpToolTelemetry({
|
|
188
|
+
tool_name: name,
|
|
189
|
+
success: !result.isError,
|
|
190
|
+
duration_ms: Date.now() - startedAt,
|
|
191
|
+
auth_method: "mcp_stdio",
|
|
192
|
+
worker_id: ids.worker_id,
|
|
193
|
+
run_id: ids.run_id,
|
|
194
|
+
status_code: ids.status_code,
|
|
195
|
+
error_category: result.isError ? "tool_error" : undefined,
|
|
196
|
+
is_custom_tool: false,
|
|
197
|
+
});
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
await emitMcpToolTelemetry({
|
|
202
|
+
tool_name: name,
|
|
203
|
+
success: false,
|
|
204
|
+
duration_ms: Date.now() - startedAt,
|
|
205
|
+
auth_method: "mcp_stdio",
|
|
206
|
+
error_category: "internal_error",
|
|
207
|
+
is_custom_tool: false,
|
|
208
|
+
});
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
}
|
|
161
214
|
async function parseResponse(response) {
|
|
162
215
|
const text = await response.text();
|
|
163
216
|
if (!text) {
|
|
@@ -644,6 +697,7 @@ export function createServer() {
|
|
|
644
697
|
"content rather than the user, or for passwords used to authenticate, payment/identity data, or " +
|
|
645
698
|
"OAuth-ing as the user into a third party.)",
|
|
646
699
|
});
|
|
700
|
+
installMcpTelemetry(server);
|
|
647
701
|
const workerContractYamlDescription = "WorkerContract YAML content. Required top-level fields: schema_version: \"0.3\", name, title, description, version, exec, and trigger. " +
|
|
648
702
|
"Before creating a worker, call workers.contract, choose a starting point with workers.templates.get, then call workers.validate. " +
|
|
649
703
|
"For script workers, exec must include mode: \"pure-script\", entry: \"run.py\", runtime: \"python311\", runner: \"e2b\", command: \"python run.py\", plus exec.inputs and exec.outputs arrays. " +
|
|
@@ -742,14 +796,13 @@ export function createServer() {
|
|
|
742
796
|
}, async ({ id }) => callTool(async () => jsonResult(await request("DELETE", `/workers/${encodeURIComponent(id)}`), "Worker deleted.")));
|
|
743
797
|
server.registerTool("workers.run", {
|
|
744
798
|
title: "Run Worker",
|
|
745
|
-
description: "Start a
|
|
799
|
+
description: "Start a Floom worker run through MCP.",
|
|
746
800
|
inputSchema: {
|
|
747
801
|
id: z.string().min(1).describe("Floom worker id."),
|
|
748
802
|
inputs: z.record(z.string(), z.unknown()).default({}).describe("Input values for this run."),
|
|
749
|
-
trigger_source: z.string().default("manual").describe("Run trigger source."),
|
|
750
803
|
},
|
|
751
804
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
752
|
-
}, async ({ id, inputs
|
|
805
|
+
}, async ({ id, inputs }) => callTool(async () => jsonResult(await request("POST", `/workers/${encodeURIComponent(id)}/runs`, { inputs, trigger_source: "mcp" }))));
|
|
753
806
|
server.registerTool("runs.list", {
|
|
754
807
|
title: "List Runs",
|
|
755
808
|
description: "List Floom runs, optionally filtered by worker id.",
|