@frontera-sdk/cli 1.52.48 → 1.53.1
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/package.json +4 -4
- package/src/api/platform-api.ts +16 -0
- package/src/commands/local/dev.ts +7 -1
- package/src/commands/mcp-server/index-commands.ts +96 -0
- package/src/commands/registry.ts +3 -0
- package/src/local/local-applied.ts +3 -0
- package/src/local/local-apps.ts +1 -1
- package/src/local/local-build-summary.ts +1 -1
- package/src/local/local-dev-sync.ts +22 -4
- package/src/local/local-function-shared.ts +1 -1
- package/src/local/local-mcp-servers.ts +66 -0
- package/src/local/local-new.ts +1 -1
- package/src/local/local-operating.ts +106 -11
- package/src/local/local-orphans.ts +29 -4
- package/src/local/local-scaffold.ts +2 -2
- package/src/local/local-schemas.ts +25 -1
- package/src/mcp/tools/docs-tools.ts +1 -1
- package/src/vendor/sdk-sources.json +17 -17
|
@@ -28,13 +28,15 @@ import type { LocalCredentials } from './local-state'
|
|
|
28
28
|
* switched on again when its file is deployed again
|
|
29
29
|
* knowledge base left as it is: deleting one takes its documents with it and
|
|
30
30
|
* cannot be undone, so `status` only says it
|
|
31
|
+
* MCP server archived (its Function runs keep naming it) — saved again,
|
|
32
|
+
* as a new server of the same slug, when its file returns
|
|
31
33
|
*
|
|
32
34
|
* An orphan the project never had (made in Studio, say) is only said: it may
|
|
33
35
|
* be work someone means to pull into the files. A Function switched off with
|
|
34
36
|
* no file is not one: it doesn't run, and there is nothing left to do.
|
|
35
37
|
*/
|
|
36
38
|
|
|
37
|
-
export type OrphanKind = 'agent' | 'workflow' | 'function' | 'knowledge'
|
|
39
|
+
export type OrphanKind = 'agent' | 'workflow' | 'function' | 'knowledge' | 'mcp-server'
|
|
38
40
|
|
|
39
41
|
export interface Orphan {
|
|
40
42
|
/** `agent/assistant`, `workflow/hello`, `function/word-count`, `knowledge/Field desk policies`. */
|
|
@@ -57,6 +59,7 @@ export interface RuntimeItems {
|
|
|
57
59
|
workflows?: Array<{ slug: string }>
|
|
58
60
|
functions?: Array<{ slug: string; enabled: boolean | null }>
|
|
59
61
|
knowledge?: Array<{ id: string; name: string }>
|
|
62
|
+
mcpServers?: Array<{ slug: string }>
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function rowsOf<T>(body: unknown): T[] {
|
|
@@ -95,13 +98,15 @@ export function orphanCalls(serviceUrl: string, creds: Pick<LocalCredentials, 'w
|
|
|
95
98
|
|
|
96
99
|
/** The Agents, workflows, Functions and knowledge bases on the runtime; each kind that didn't answer is left out. */
|
|
97
100
|
export async function runtimeItems(call: Call, workspaceId: string): Promise<RuntimeItems> {
|
|
98
|
-
const [agents, workflows, functions, knowledge] = await Promise.all([
|
|
101
|
+
const [agents, workflows, functions, knowledge, mcpServers] = await Promise.all([
|
|
99
102
|
// The sync listing: drafts never published included, archived and platform Agents left out.
|
|
100
103
|
call('/v1/sync/agents'),
|
|
101
104
|
// Archived workflows are left out of the listing.
|
|
102
105
|
call('/v1/workflows'),
|
|
103
106
|
call('/v1/functions'),
|
|
104
107
|
call(`/v1/workspace-knowledge?workspaceId=${encodeURIComponent(workspaceId)}`),
|
|
108
|
+
// Archived servers are left out of the listing.
|
|
109
|
+
call('/v1/mcp-servers'),
|
|
105
110
|
])
|
|
106
111
|
const out: RuntimeItems = {}
|
|
107
112
|
if (agents.ok) {
|
|
@@ -113,15 +118,20 @@ export async function runtimeItems(call: Call, workspaceId: string): Promise<Run
|
|
|
113
118
|
if (workflows.ok) out.workflows = rowsOf<{ slug?: string; archivedAt?: string | null }>(workflows.body).filter((w) => w.slug && !w.archivedAt).map((w) => ({ slug: w.slug! }))
|
|
114
119
|
if (functions.ok) out.functions = rowsOf<{ slug?: string; enabled?: boolean | null }>(functions.body).filter((f) => f.slug).map((f) => ({ slug: f.slug!, enabled: f.enabled ?? null }))
|
|
115
120
|
if (knowledge.ok) out.knowledge = rowsOf<{ id?: string; name?: string }>(knowledge.body).filter((k) => k.id && k.name).map((k) => ({ id: k.id!, name: k.name! }))
|
|
121
|
+
if (mcpServers.ok) {
|
|
122
|
+
const data = (mcpServers.body as { data?: { servers?: unknown } } | null)?.data?.servers
|
|
123
|
+
out.mcpServers = (Array.isArray(data) ? data as Array<{ slug?: string }> : []).filter((m) => m.slug).map((m) => ({ slug: m.slug! }))
|
|
124
|
+
}
|
|
116
125
|
return out
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
/** The names the project's files give each kind: an Agent's folder, a workflow's file, a Function's file or folder, a knowledge base's `name`. */
|
|
120
129
|
export function projectNames(root: string): Record<OrphanKind, Set<string>> {
|
|
121
|
-
const out: Record<OrphanKind, Set<string>> = { agent: new Set(), workflow: new Set(), function: new Set(), knowledge: new Set() }
|
|
130
|
+
const out: Record<OrphanKind, Set<string>> = { agent: new Set(), workflow: new Set(), function: new Set(), knowledge: new Set(), 'mcp-server': new Set() }
|
|
122
131
|
for (const t of allTargets(root)) {
|
|
123
132
|
if (t.kind === 'agent') out.agent.add(t.slug)
|
|
124
133
|
else if (t.kind === 'workflow') out.workflow.add(t.slug)
|
|
134
|
+
else if (t.kind === 'mcp-server') out['mcp-server'].add(t.name)
|
|
125
135
|
else if (t.kind === 'function') {
|
|
126
136
|
const slug = functionSlugOf(t.key)
|
|
127
137
|
if (slug) out.function.add(slug)
|
|
@@ -143,6 +153,7 @@ export function appliedKeysOf(kind: OrphanKind, name: string): string[] {
|
|
|
143
153
|
if (kind === 'agent') return [`agent:${name}`, `operating:${name}`, `bindings:${name}`, `lessons:${name}`]
|
|
144
154
|
if (kind === 'workflow') return [`workflow:${name}`]
|
|
145
155
|
if (kind === 'function') return [`function:${name}.ts`, `function:${name}/`]
|
|
156
|
+
if (kind === 'mcp-server') return [`mcp-server:${name}`]
|
|
146
157
|
return []
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -153,13 +164,14 @@ function wasApplied(kind: OrphanKind, name: string, applied: ReadonlySet<string>
|
|
|
153
164
|
return appliedKeysOf(kind, name).some((k) => applied.has(k))
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
const WORDS: Record<OrphanKind, string> = { agent: 'Agent', workflow: 'workflow', function: 'Function', knowledge: 'knowledge base' }
|
|
167
|
+
const WORDS: Record<OrphanKind, string> = { agent: 'Agent', workflow: 'workflow', function: 'Function', knowledge: 'knowledge base', 'mcp-server': 'MCP server' }
|
|
157
168
|
|
|
158
169
|
/** The file an item of a kind would have in the project. */
|
|
159
170
|
function fileOf(kind: OrphanKind, name: string): string {
|
|
160
171
|
if (kind === 'agent') return `agents/${name}/agent.json`
|
|
161
172
|
if (kind === 'workflow') return `workflows/${name}.yaml`
|
|
162
173
|
if (kind === 'function') return `functions/${name}.ts`
|
|
174
|
+
if (kind === 'mcp-server') return `mcp-servers/${name}.yaml`
|
|
163
175
|
return 'knowledge/<folder>/knowledge.json'
|
|
164
176
|
}
|
|
165
177
|
|
|
@@ -183,6 +195,10 @@ export function findOrphans(root: string, items: RuntimeItems, applied: Readonly
|
|
|
183
195
|
fix = deleted
|
|
184
196
|
? `\`frontera local dev\` switches it off (the runtime can’t remove a Function), or \`frontera function disable ${name}\` now`
|
|
185
197
|
: `add its file to the project, or \`frontera function disable ${name}\` to switch it off (the runtime can’t remove a Function)`
|
|
198
|
+
} else if (kind === 'mcp-server') {
|
|
199
|
+
fix = deleted
|
|
200
|
+
? '`frontera local dev` archives it (or `frontera mcp-server archive ' + name + '` now)'
|
|
201
|
+
: `add its file to the project (mcp-servers/${name}.yaml), or \`frontera mcp-server archive ${name}\` if it isn’t wanted`
|
|
186
202
|
} else fix = 'add its folder to the project, or delete it from its page in Studio if it isn’t wanted: deleting a knowledge base takes its documents with it, so `frontera local dev` never does'
|
|
187
203
|
out.push({ ref: `${kind}/${name}`, kind, name, ...(id ? { id } : {}), deleted, problem, fix })
|
|
188
204
|
}
|
|
@@ -191,6 +207,7 @@ export function findOrphans(root: string, items: RuntimeItems, applied: Readonly
|
|
|
191
207
|
// A Function switched off and with no file: it doesn't run, and the runtime can't remove it, so there is nothing to fix.
|
|
192
208
|
for (const f of items.functions ?? []) if (f.enabled !== false) add('function', f.slug)
|
|
193
209
|
for (const k of items.knowledge ?? []) add('knowledge', k.name, k.id)
|
|
210
|
+
for (const m of items.mcpServers ?? []) add('mcp-server', m.slug)
|
|
194
211
|
return out
|
|
195
212
|
}
|
|
196
213
|
|
|
@@ -274,6 +291,14 @@ export async function retireDeleted(project: Pick<LocalProject, 'stateDir'>, org
|
|
|
274
291
|
}
|
|
275
292
|
retired[o.ref] = { how: 'switched-off', at }
|
|
276
293
|
lines.push(` switched off Function ${o.name}: its file is gone from the project, and the runtime can’t remove a Function (it is switched on again if the file comes back)`)
|
|
294
|
+
} else if (o.kind === 'mcp-server') {
|
|
295
|
+
const res = await call(`/v1/mcp-servers/${encodeURIComponent(o.name)}`, { method: 'DELETE' })
|
|
296
|
+
if (!res.ok && res.status !== 404) {
|
|
297
|
+
lines.push(` warning: MCP server ${o.name}’s file is gone, but it couldn’t be archived here: ${said(res)}`)
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
retired[o.ref] = { how: 'archived', at }
|
|
301
|
+
lines.push(` archived MCP server ${o.name}: its file is gone from the project, so clients can no longer connect to it (it is saved again if the file comes back)`)
|
|
277
302
|
} else continue
|
|
278
303
|
forget.push(...appliedKeysOf(o.kind, o.name))
|
|
279
304
|
}
|
|
@@ -176,7 +176,7 @@ A Frontera project. Every file here is applied to a local runtime by
|
|
|
176
176
|
| Path | What it is |
|
|
177
177
|
|---|---|
|
|
178
178
|
| \`agents/<slug>/agent.json\` | An Agent's draft: model, instructions, tools |
|
|
179
|
-
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks |
|
|
179
|
+
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks, handover reasons |
|
|
180
180
|
| \`agents/<slug>/bindings.json\` | What it may use: Functions, workflows, Blueprint object types |
|
|
181
181
|
| \`agents/<slug>/evals/*.yaml\` | Its test cases |
|
|
182
182
|
| \`workflows/<slug>.yaml\` | A workflow |
|
|
@@ -301,7 +301,7 @@ Frontera deployment.
|
|
|
301
301
|
| Path | What it is |
|
|
302
302
|
|---|---|
|
|
303
303
|
| \`agents/<slug>/agent.json\` | An Agent's draft: model, instructions, tools |
|
|
304
|
-
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks |
|
|
304
|
+
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks, handover reasons |
|
|
305
305
|
| \`agents/<slug>/bindings.json\` | What it may use: the Functions and workflows it may start, the Blueprint object types it may query |
|
|
306
306
|
| \`agents/<slug>/evals/*.yaml\` | Its test cases: a conversation and what a good reply does |
|
|
307
307
|
| \`workflows/<slug>.yaml\` | A workflow |
|
|
@@ -6,13 +6,14 @@ import { BLUEPRINT_SCHEMA_FILES, BLUEPRINT_SCHEMAS } from './local-blueprint-sch
|
|
|
6
6
|
import type { LocalProject } from './local-project'
|
|
7
7
|
import { isAlive, readState } from './local-state'
|
|
8
8
|
import { PROCEDURE_TOOL_FORMS, PROCEDURE_TOOL_GROUPS } from './local-agent-checks'
|
|
9
|
+
import { DEFAULT_HANDOVER_REASON_KEYS } from './local-operating'
|
|
9
10
|
import { REF_KINDS, refPattern, type RefKind } from './local-sync-refs'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The formats of a project's files, as JSON Schema, for the coding agents and
|
|
13
14
|
* editors working in it (`frontera local schema <kind>`).
|
|
14
15
|
*
|
|
15
|
-
* The service checks
|
|
16
|
+
* The service checks four of them (an Agent, its behavior, a workflow, an MCP server), so their schemas come from the service
|
|
16
17
|
* the runtime runs (`GET /v1/config/file-schemas/<kind>`), made from the same
|
|
17
18
|
* zod schemas that check the files, then widened where a project file names a
|
|
18
19
|
* resource by reference instead of id (`localizeServiceSchema`). The others
|
|
@@ -34,6 +35,7 @@ export const SCHEMA_KINDS = [
|
|
|
34
35
|
'theme',
|
|
35
36
|
'chat-app',
|
|
36
37
|
'app',
|
|
38
|
+
'mcp-server',
|
|
37
39
|
'knowledge',
|
|
38
40
|
'skill',
|
|
39
41
|
'blueprint-object-type',
|
|
@@ -60,6 +62,7 @@ export const SCHEMA_FILES: Record<SchemaKind, string> = {
|
|
|
60
62
|
theme: 'themes/<name>/theme.json',
|
|
61
63
|
'chat-app': 'chat-apps/<slug>.json',
|
|
62
64
|
app: 'apps/<slug>.json',
|
|
65
|
+
'mcp-server': 'mcp-servers/<slug>.yaml',
|
|
63
66
|
knowledge: 'knowledge/<name>/knowledge.json',
|
|
64
67
|
skill: 'skills/<name>/SKILL.md (its front matter)',
|
|
65
68
|
secrets: 'secrets.json',
|
|
@@ -888,6 +891,27 @@ function describeProjectFields(props: Record<string, unknown>): void {
|
|
|
888
891
|
if ('dataScopes' in props && 'variables' in props) {
|
|
889
892
|
describe(props, 'dataScopes', 'Optional. Narrows what the Agent reads of an object type to the records of the conversation: `[{ "objectType": "WorkOrder", "property": "customerId", "equals": "{{customer_id}}" }]`, at most one per object type. Every read of that type (query, fetch, aggregate, saved sets, links, media and metrics) sees only rows whose property equals the conversation variable; a conversation without the variable sees none. The variable is one in `variables`, and the property and variable are text or numbers. The object type must be granted in bindings.json `blueprint`. In an eval case, `variables` gives the conversation its values.')
|
|
890
893
|
}
|
|
894
|
+
// Behavior settings (operating.json): why the Agent hands a conversation to people.
|
|
895
|
+
if ('handoverReasons' in props && 'variables' in props) {
|
|
896
|
+
const reasons = props.handoverReasons
|
|
897
|
+
const item = isRecord(reasons) && isRecord(reasons.items) ? reasons.items : null
|
|
898
|
+
if (isRecord(reasons) && item && isRecord(item.properties)) {
|
|
899
|
+
const inner = { ...item.properties }
|
|
900
|
+
describe(inner, 'key', 'The reason’s id: lower snake case (`order_problem`), at most 40 characters, unique in the list. Hand-over reports count by it, so keep it once the Agent is published; a handoff action names it in `handoverReason`.')
|
|
901
|
+
describe(inner, 'name', 'What people see in hand-over reports and the inbox (`Order problem`), 1 to 60 characters.')
|
|
902
|
+
describe(inner, 'whenToUse', 'What the Agent reads to choose this reason (`The customer reports a late, wrong or damaged order.`), 1 to 300 characters.')
|
|
903
|
+
props.handoverReasons = { ...reasons, items: { ...item, properties: inner } }
|
|
904
|
+
}
|
|
905
|
+
describe(props, 'handoverReasons', `Optional. Why the Agent hands a conversation to people; hand-over reports count by these. 1 to 15 reasons, each \`{ "key", "name", "whenToUse" }\`, keys unique, names unique ignoring case, and one must have the key \`other\` (rename it, never remove it). Leave the key out to use the default reasons (${DEFAULT_HANDOVER_REASON_KEYS.map((k) => `\`${k}\``).join(', ')}); a pull writes it only when the Agent has its own. \`frontera local dev\` refuses a list that breaks these rules, with each problem's path.`)
|
|
906
|
+
}
|
|
907
|
+
// A procedure's action (operating.json `procedures[].actions[]`): the reason a handoff records.
|
|
908
|
+
if ('handoverReason' in props && 'requiresFields' in props) {
|
|
909
|
+
describe(props, 'handoverReason', 'Optional, for a `handoff` action: the `key` of one of the Agent’s `handoverReasons` (the defaults when the file has none), recorded on the hand-over when a Step by step procedure runs the action. Absent: `other`. A key the Agent doesn’t have is reported by `frontera local dev` and refuses publishing.')
|
|
910
|
+
}
|
|
911
|
+
// A Step by step End step (operating.json `procedures[].blocks[].steps[]`): the reason one that hands over records.
|
|
912
|
+
if ('handoverReason' in props && 'outcome' in props) {
|
|
913
|
+
describe(props, 'handoverReason', 'Optional, for an End step with `outcome` `handoff`: the `key` of one of the Agent’s `handoverReasons` (the defaults when the file has none), recorded on the hand-over. Absent: `other`. A key the Agent doesn’t have is reported by `frontera local dev` and refuses publishing.')
|
|
914
|
+
}
|
|
891
915
|
// An Agent (agent.json): the lists that name other resources.
|
|
892
916
|
if ('modelConfigs' in props && 'knowledgeAttachments' in props) {
|
|
893
917
|
describe(props, 'knowledgeAttachments', 'The knowledge bases the Agent searches: `[{ "knowledgeConfigId": "knowledge/<name>" }]`, one per base in knowledge/<name>/knowledge.json (`retrievalOverride` optional). `local dev` resolves each to this runtime’s id. When the key is present it is the Agent’s whole list (a base it leaves out is detached); leave the key out to keep the attachments made with `frontera knowledge attach`.', {
|
|
@@ -12,7 +12,7 @@ const DOCS_TOOLS = [
|
|
|
12
12
|
{ name: 'search_documentation', description: "Search Frontera's documentation for the release the local runtime runs. Returns guides with the best matching heading and a snippet; read one with read_documentation." },
|
|
13
13
|
{ name: 'read_documentation', description: 'Read a guide by its slug from a search result, whole or one heading of it.' },
|
|
14
14
|
{ name: 'list_documentation', description: 'List every guide: slug, title and description. Use it to browse an area, e.g. everything under "workflows/".' },
|
|
15
|
-
{ name: 'read_file_schema', description: 'The exact format of a kind of project file (agent.json, operating.json, a workflow, the Blueprint), as JSON Schema, for the runtime\'s release. Read it before writing such a file.' },
|
|
15
|
+
{ name: 'read_file_schema', description: 'The exact format of a kind of project file (agent.json, operating.json, a workflow, an MCP server, the Blueprint), as JSON Schema, for the runtime\'s release. Read it before writing such a file.' },
|
|
16
16
|
] as const
|
|
17
17
|
|
|
18
18
|
export function docsTools(project: LocalProject, schemaFor: SchemaLookup): McpTool[] {
|