@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.
@@ -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 three of them, so their schemas come from the service
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[] {