biaws 0.5.3 → 0.6.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/README.md CHANGED
@@ -3,6 +3,10 @@
3
3
  CLI do Bondia Workspaces para administrar instalações, configurar o acesso
4
4
  global e operar os recursos do workspace associado à pasta atual.
5
5
 
6
+ Setup/update aceitam MCP HTTP/OAuth opcional. Veja
7
+ [configuração ChatGPT](docs/chatgpt.md) e o
8
+ [runbook de operação](../docs/chatgpt-operations.md).
9
+
6
10
  ## Instalação
7
11
 
8
12
  ```bash
@@ -0,0 +1,19 @@
1
+ # Conexão direta ao ChatGPT
2
+
3
+ Habilite com `biaws admin instance setup --mcp-http` ou
4
+ `biaws admin instance update NOME --mcp-http`. URL pública exige HTTPS; loopback
5
+ HTTP é apenas para desenvolvimento. Atualizar uma instância continua sujeito ao
6
+ fluxo de backup do comando update. O CLI gera segredo aleatório dedicado em
7
+ arquivo 0600 por instância e preserva-o em reconfigurações.
8
+
9
+ `update NOME --no-mcp-http` desabilita a entrada, preservando replica set,
10
+ configuração e dados OAuth. `status NOME --diagnose-mcp --json` verifica discovery,
11
+ issuer/resource, PKCE, API e challenge sem solicitar senha. Os resultados refletem
12
+ o acesso desta máquina; não comprovam acesso pela infraestrutura do ChatGPT.
13
+
14
+ `biaws workspace agent configure chatgpt --url https://instancia.example --json`
15
+ mostra a URL /mcp e os passos de login/consentimento no navegador. Acrescente
16
+ `--diagnose` para verificações de rede. Esse comando não exige API key nem
17
+ escreve credenciais no projeto. Codex/Claude continuam com stdio.
18
+ A URL pública e o workspace autorizado são escolhas diferentes: cada conexão
19
+ OAuth é vinculada no servidor ao workspace selecionado no consentimento.
@@ -137,3 +137,17 @@ O restore valida checksum, descriptografia, versão e entradas do tar antes de
137
137
  substituir dados. Portas, URLs e caminhos de storage do destino são
138
138
  preservados. Em falhas após a pausa, o mecanismo tenta recompor os serviços e
139
139
  mantém o erro original como evidência.
140
+
141
+ O schema 2 inclui a versão MCP implantável e aceita a leitura dos snapshots de
142
+ schema 1. Use CLI >=0.6.0; uma mudança isolada de MCP passa a exigir update.
143
+ Start/stop/status/update conservam o overlay e o perfil persistidos por instância.
144
+ Ao desabilitar, o update para o container MCP e mantém o replica set e suas chaves.
145
+ Os scripts gerados e os comandos de backup/restore usam o mesmo seletor de Compose.
146
+
147
+ Backup criptografado inclui o segredo dedicado e todo o banco, incluindo chaves
148
+ de assinatura, concessões e tokens. Restore mantém URL, flags e paths do destino
149
+ e restaura o segredo em arquivo privado. Se a origem pública mudou, remove os
150
+ dados OAuth e as chaves de assinatura restaurados antes de reiniciar: cada usuário
151
+ deve autorizar uma nova conexão. O snapshot de release continua invalidado até
152
+ um update bem-sucedido. Restore não habilita MCP automaticamente em um destino
153
+ que não estava configurado para ele.
package/docs/releasing.md CHANGED
@@ -48,6 +48,14 @@ global. Depois da publicação, confirme também `npx biaws@<versão> --help`.
48
48
 
49
49
  ## Compatibilidade e migração
50
50
 
51
+ A série `0.6.0` suporta os manifests de plataforma schema 1 e 2 e fixa
52
+ `biaws-mcp@0.15.0`. Publique esse MCP antes de `biaws@0.6.0`; o CLI antigo
53
+ não deve aplicar o manifesto schema 2. Atualizar somente o CLI não atualiza
54
+ instâncias. Para MCP HTTP, use setup/update com `--mcp-http` e os requisitos
55
+ HTTPS/replica set de [operação ChatGPT](../../docs/chatgpt-operations.md).
56
+ `workspace agent configure chatgpt --url https://<host> --json` fornece
57
+ instruções sem API key ou senha; a conta é autenticada no consentimento OAuth.
58
+
51
59
  O shebang canônico está em `biaws-cli/bin/biaws.js`. O pacote npm cria o comando
52
60
  `biaws`; no checkout, `npm --prefix biaws-cli link` oferece a mesma experiência.
53
61
  O antigo `node biaws-cli/src/index.js ...` continua funcional durante a série
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biaws",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "CLI do Bondia Workspaces para administração, configuração e operação de workspaces",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1 @@
1
+ export { default } from "../../../../commands/configure/chatgpt.js";
@@ -11,7 +11,7 @@ import { runSkillsCommand } from "./skills.js";
11
11
  const CODEX_BEGIN = "# BEGIN BIAWS MANAGED MCP";
12
12
  const CODEX_END = "# END BIAWS MANAGED MCP";
13
13
  export const MCP_PACKAGE_NAME = "biaws-mcp";
14
- export const MCP_PACKAGE_VERSION = "0.14.1";
14
+ export const MCP_PACKAGE_VERSION = "0.15.0";
15
15
  export const MCP_PACKAGE_SPEC = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
16
16
  const MCP_COMMAND = "npx";
17
17
  const MCP_ARGS = ["--yes", MCP_PACKAGE_SPEC];
@@ -0,0 +1,17 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../baseCommands.js";
3
+ import { chatgptConnectionInstructions, diagnoseMcp } from "../../instance/mcp.js";
4
+
5
+ export default class ConfigureChatgpt extends BaseCommand {
6
+ static description = "orienta conexão direta ao ChatGPT usando OAuth, sem capturar senha ou chave";
7
+ static flags = {
8
+ url: Flags.string({ description: "origem pública HTTPS da instância", required: true }),
9
+ diagnose: Flags.boolean({ description: "verifica discovery, saúde e challenge a partir desta máquina" }),
10
+ json: Flags.boolean({ description: "emite JSON" }),
11
+ };
12
+ async run() {
13
+ const { flags } = await this.parse(ConfigureChatgpt);
14
+ const result = flags.diagnose ? await diagnoseMcp(flags.url) : chatgptConnectionInstructions(flags.url);
15
+ this.output({ json: flags.json }).result(flags.json ? result : JSON.stringify(result, null, 2));
16
+ }
17
+ }
@@ -14,6 +14,7 @@ import {
14
14
  const setupFlags = {
15
15
  ...contextFlags,
16
16
  ...wizardFlags,
17
+ "mcp-http": Flags.boolean({ allowNo: true, description: "habilita MCP HTTP/OAuth (HTTPS público ou loopback)" }),
17
18
  name: Flags.string({ char: "n", description: "nome da instância" }),
18
19
  "admin-email": Flags.string({
19
20
  description: "e-mail do administrador inicial",
@@ -287,7 +288,10 @@ export default class InstanceSetup extends LocalInstanceCommand {
287
288
  terminal: this.adapters.terminal,
288
289
  });
289
290
  const configuration = await buildSetupConfiguration(
290
- Object.fromEntries(definition.questions.map((question) => [question.name, plan.get(question.name)])),
291
+ {
292
+ ...Object.fromEntries(definition.questions.map((question) => [question.name, plan.get(question.name)])),
293
+ mcpHttp: flags["mcp-http"],
294
+ },
291
295
  context,
292
296
  this.adapters.filesystem,
293
297
  );
@@ -3,12 +3,14 @@ import { Flags } from "@oclif/core";
3
3
  import { LocalInstanceCommand } from "../../baseCommands.js";
4
4
  import { contextFlags, contextInput, instanceArgument, writeResult } from "../../instance/command.js";
5
5
  import { getInstance, operateInstance } from "../../instance/service.js";
6
+ import { diagnoseMcp, mcpInstanceSummary } from "../../instance/mcp.js";
6
7
 
7
8
  export default class InstanceStatus extends LocalInstanceCommand {
8
9
  static description = "consulta o estado dos containers de uma instância";
9
10
  static args = { instance: instanceArgument };
10
11
  static flags = {
11
12
  ...contextFlags,
13
+ "diagnose-mcp": Flags.boolean({ description: "valida discovery e saúde da integração OAuth/MCP" }),
12
14
  json: Flags.boolean({ description: "emite JSON" }),
13
15
  };
14
16
 
@@ -17,6 +19,14 @@ export default class InstanceStatus extends LocalInstanceCommand {
17
19
  const context = await this.localContext(contextInput(flags, args.instance));
18
20
  const instance = await getInstance(context, this.adapters.filesystem, args.instance);
19
21
  const result = await operateInstance(instance, context, this.adapters.processRunner, "status");
20
- writeResult(this, result, (value) => value.output || `Nenhum container ativo para ${value.instance}.`, flags.json);
22
+ result.mcp = mcpInstanceSummary(instance);
23
+ if (flags["diagnose-mcp"]) result.mcp.diagnostics = await diagnoseMcp(instance.publicUrl);
24
+ writeResult(
25
+ this,
26
+ result,
27
+ (value) =>
28
+ (value.output || `Nenhum container ativo para ${value.instance}.`) + "\n" + JSON.stringify(value.mcp, null, 2),
29
+ flags.json,
30
+ );
21
31
  }
22
32
  }
@@ -2,6 +2,7 @@ import { Flags } from "@oclif/core";
2
2
 
3
3
  import { LocalInstanceCommand } from "../../baseCommands.js";
4
4
  import { contextFlags, contextInput, instanceArgument } from "../../instance/command.js";
5
+ import { configureMcpInstance } from "../../instance/mcp.js";
5
6
  import {
6
7
  backupArguments,
7
8
  executeInstanceScript,
@@ -85,6 +86,11 @@ export default class InstanceUpdate extends LocalInstanceCommand {
85
86
  static args = { instance: instanceArgument };
86
87
  static flags = {
87
88
  ...contextFlags,
89
+ "mcp-http": Flags.boolean({
90
+ allowNo: true,
91
+ description: "habilita/desabilita MCP HTTP, preservando configuração",
92
+ exclusive: ["check"],
93
+ }),
88
94
  "backup-output": Flags.string({
89
95
  description: "arquivo de destino do backup anterior à atualização",
90
96
  }),
@@ -109,19 +115,24 @@ export default class InstanceUpdate extends LocalInstanceCommand {
109
115
  async run() {
110
116
  const { args, flags } = await this.parse(InstanceUpdate);
111
117
  const context = await this.localContext(contextInput(flags, args.instance));
112
- const instance = await getInstance(context, this.adapters.filesystem, args.instance);
118
+ let instance = await getInstance(context, this.adapters.filesystem, args.instance);
113
119
  const versionStatus = await getInstanceUpdateStatus(instance, context, this.adapters.filesystem);
114
120
  if (flags.check) {
115
121
  this.output({ json: flags.json }).result(versionCheckResult(instance, versionStatus, flags.json));
116
122
  return;
117
123
  }
118
- if (!versionStatus.updateRequired && !flags.force) {
124
+ if (!versionStatus.updateRequired && !flags.force && flags["mcp-http"] === undefined) {
119
125
  this.output({ json: flags.json }).result(unchangedResult(instance, versionStatus, flags.json));
120
126
  return;
121
127
  }
122
128
 
123
129
  await validateInstanceUpdate(instance, context, this.adapters.processRunner);
124
130
  const backupOutput = await createUpdateBackup(this, instance, context, flags);
131
+ if (flags["mcp-http"] !== undefined) {
132
+ await configureMcpInstance(instance.envFile, flags["mcp-http"], this.adapters.filesystem);
133
+ instance = await getInstance(context, this.adapters.filesystem, args.instance);
134
+ await validateInstanceUpdate(instance, context, this.adapters.processRunner);
135
+ }
125
136
 
126
137
  await updateInstance(
127
138
  instance,
@@ -32,5 +32,6 @@ export function instanceSummary(instance) {
32
32
  `API: http://127.0.0.1:${instance.apiPort}`,
33
33
  `UI: ${instance.publicUrl}`,
34
34
  `Storage: ${instance.storage}`,
35
+ `MCP HTTP: ${instance.mcp?.enabled ? instance.mcp.resource : "desabilitado"}`,
35
36
  ].join("\n");
36
37
  }
@@ -0,0 +1,169 @@
1
+ import path from "node:path";
2
+ import { randomBytes } from "node:crypto";
3
+ import { CliError } from "../core/errors.js";
4
+ import { parseEnv } from "../core/context.js";
5
+ import { atomicWrite } from "./release.js";
6
+
7
+ export function mcpPublicConfiguration(publicUrl) {
8
+ let url;
9
+ try {
10
+ url = new URL(publicUrl);
11
+ } catch {
12
+ throw new CliError("Informe uma origem pública HTTPS válida.", { code: "INVALID_MCP_PUBLIC_URL" });
13
+ }
14
+ if (
15
+ url.username ||
16
+ url.password ||
17
+ url.pathname !== "/" ||
18
+ url.search ||
19
+ url.hash ||
20
+ (url.protocol !== "https:" &&
21
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
22
+ ) {
23
+ throw new CliError("MCP exige HTTPS público; HTTP é permitido somente em loopback para desenvolvimento.", {
24
+ code: "INVALID_MCP_PUBLIC_URL",
25
+ });
26
+ }
27
+ return {
28
+ publicUrl: url.origin,
29
+ resource: url.origin + "/mcp",
30
+ issuer: url.origin + "/api/auth",
31
+ scheme: url.protocol.slice(0, -1),
32
+ };
33
+ }
34
+ export function mcpInstanceSummary(instance) {
35
+ return {
36
+ configured: instance.env.BIAWS_MCP_CONFIGURED === "true",
37
+ enabled: instance.env.BIAWS_MCP_HTTP_ENABLED === "true",
38
+ resource: instance.env.BIAWS_MCP_PUBLIC_URL || "",
39
+ issuer: instance.env.BIAWS_PUBLIC_URL ? instance.env.BIAWS_PUBLIC_URL + "/api/auth" : "",
40
+ };
41
+ }
42
+ function replaceEnvironment(contents, values) {
43
+ let next = contents;
44
+ for (const [key, value] of Object.entries(values)) {
45
+ const line = key + "=" + value;
46
+ const pattern = new RegExp("^" + key + "=.*$", "gmu");
47
+ next = pattern.test(next) ? next.replace(pattern, () => line) : next.replace(/\s*$/u, "") + "\n" + line + "\n";
48
+ }
49
+ return next;
50
+ }
51
+ export async function configureMcpInstance(
52
+ envFile,
53
+ enabled,
54
+ filesystem,
55
+ { uid = process.getuid?.() ?? 1000, gid = process.getgid?.() ?? 1000 } = {},
56
+ ) {
57
+ const contents = await filesystem.readFile(envFile, "utf8");
58
+ const env = parseEnv(contents);
59
+ const configured = enabled || env.BIAWS_MCP_CONFIGURED === "true";
60
+ const values = { BIAWS_MCP_HTTP_ENABLED: String(enabled) };
61
+ if (configured) {
62
+ const config = mcpPublicConfiguration(env.BIAWS_PUBLIC_URL);
63
+ const secretPath = env.BIAWS_MCP_SERVICE_SECRET_PATH || path.join(path.dirname(envFile), ".mcp-service-secret");
64
+ if (/[\r\n$#"'\\]/u.test(secretPath))
65
+ throw new CliError("Caminho de segredo incompatível com Compose.", { code: "INVALID_MCP_SECRET_PATH" });
66
+ let secret;
67
+ try {
68
+ secret = await filesystem.readFile(secretPath, "utf8");
69
+ } catch (error) {
70
+ if (error.code !== "ENOENT") throw error;
71
+ secret = randomBytes(32).toString("hex");
72
+ await filesystem.writeFile(secretPath, secret, { mode: 0o600, flag: "wx" });
73
+ }
74
+ if (secret.trim().length < 32 || secret.length > 4096)
75
+ throw new CliError("Segredo MCP inválido; use um arquivo dedicado de 32 a 4096 caracteres.", {
76
+ code: "INVALID_MCP_SERVICE_SECRET",
77
+ });
78
+ await filesystem.chmod(secretPath, 0o600);
79
+ Object.assign(values, {
80
+ BIAWS_MCP_CONFIGURED: "true",
81
+ BIAWS_MCP_PUBLIC_URL: config.resource,
82
+ BIAWS_PUBLIC_SCHEME: config.scheme,
83
+ BIAWS_MCP_SERVICE_SECRET_PATH: secretPath,
84
+ BIAWS_MCP_UID: env.BIAWS_MCP_UID || uid,
85
+ BIAWS_MCP_GID: env.BIAWS_MCP_GID || gid,
86
+ });
87
+ }
88
+ await atomicWrite(envFile, replaceEnvironment(contents, values), filesystem);
89
+ return { configured, enabled, ...(configured ? mcpPublicConfiguration(env.BIAWS_PUBLIC_URL) : {}) };
90
+ }
91
+ export async function diagnoseMcp(publicUrl, { fetchImpl = fetch } = {}) {
92
+ const config = mcpPublicConfiguration(publicUrl);
93
+ const checks = [];
94
+ const get = async (name, url) => {
95
+ try {
96
+ const response = await fetchImpl(url, { redirect: "error", signal: AbortSignal.timeout(5000) });
97
+ if (!response.ok) throw new Error("HTTP " + response.status);
98
+ const result = await response.json();
99
+ checks.push({ name, ok: true });
100
+ return result;
101
+ } catch {
102
+ checks.push({ name, ok: false, message: "Não foi possível ler " + url + ". Verifique TLS, proxy e serviço." });
103
+ return null;
104
+ }
105
+ };
106
+ const protectedResource = await get(
107
+ "resource-discovery",
108
+ config.publicUrl + "/.well-known/oauth-protected-resource/mcp",
109
+ );
110
+ const authorization = await get(
111
+ "authorization-discovery",
112
+ config.publicUrl + "/.well-known/oauth-authorization-server/api/auth",
113
+ );
114
+ if (protectedResource)
115
+ checks.push({
116
+ name: "resource-binding",
117
+ ok:
118
+ protectedResource.resource === config.resource &&
119
+ protectedResource.authorization_servers?.includes(config.issuer),
120
+ });
121
+ if (authorization)
122
+ checks.push({
123
+ name: "issuer-and-pkce",
124
+ ok: authorization.issuer === config.issuer && authorization.code_challenge_methods_supported?.includes("S256"),
125
+ });
126
+ await get("api-health", config.publicUrl + "/api/health");
127
+ try {
128
+ const response = await fetchImpl(config.resource, {
129
+ method: "POST",
130
+ redirect: "error",
131
+ signal: AbortSignal.timeout(5000),
132
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
133
+ body: JSON.stringify({
134
+ jsonrpc: "2.0",
135
+ id: 1,
136
+ method: "initialize",
137
+ params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "biaws-doctor", version: "1" } },
138
+ }),
139
+ });
140
+ checks.push({
141
+ name: "mcp-oauth-challenge",
142
+ ok:
143
+ response.status === 401 &&
144
+ (response.headers.get("www-authenticate") || "").includes("oauth-protected-resource"),
145
+ });
146
+ await response.body?.cancel();
147
+ } catch {
148
+ checks.push({ name: "mcp-oauth-challenge", ok: false });
149
+ }
150
+ return {
151
+ ...config,
152
+ ok: checks.every((check) => check.ok),
153
+ checks,
154
+ observation: "Verificações realizadas a partir desta máquina. Acessibilidade pelo ChatGPT ainda não comprovada.",
155
+ };
156
+ }
157
+ export function chatgptConnectionInstructions(publicUrl) {
158
+ const config = mcpPublicConfiguration(publicUrl);
159
+ return {
160
+ ...config,
161
+ authentication: "OAuth",
162
+ steps: [
163
+ "No ChatGPT, crie uma conexão MCP personalizada com a URL do recurso e autenticação OAuth.",
164
+ "Autentique no navegador com sua conta BIAWS, escolha o workspace e consinta os acessos.",
165
+ "Para outro workspace, crie outra autorização. Revogue conexões em Minha conta no BIAWS.",
166
+ ],
167
+ catalog: "O plugin distribuível não implica elegibilidade ou publicação no catálogo.",
168
+ };
169
+ }
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { CliError } from "../core/errors.js";
4
4
 
5
- const COMPONENTS = ["biaws-api", "biaws-ui", "biaws-monitor-executor"];
5
+ const LEGACY_COMPONENTS = ["biaws-api", "biaws-ui", "biaws-monitor-executor"];
6
+ const COMPONENTS = [...LEGACY_COMPONENTS, "biaws-mcp"];
6
7
  const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
7
8
 
8
9
  function invalid(message) {
@@ -11,22 +12,23 @@ function invalid(message) {
11
12
 
12
13
  export function validateRelease(value) {
13
14
  if (!value || typeof value !== "object" || Array.isArray(value)) invalid("Manifesto de release inválido.");
14
- if (value.schemaVersion !== 1) invalid("schemaVersion do manifesto não suportada; atualize o CLI.");
15
+ if (![1, 2].includes(value.schemaVersion)) invalid("schemaVersion do manifesto não suportada; atualize o CLI.");
16
+ const expected = value.schemaVersion === 1 ? LEGACY_COMPONENTS : COMPONENTS;
15
17
  if (typeof value.version !== "string" || !VERSION.test(value.version)) invalid("Versão da release inválida.");
16
18
  if (!Number.isSafeInteger(value.deploymentRevision) || value.deploymentRevision < 1) {
17
19
  invalid("deploymentRevision deve ser um inteiro positivo.");
18
20
  }
19
- if (!value.components || Object.keys(value.components).sort().join() !== [...COMPONENTS].sort().join()) {
20
- invalid("O manifesto deve conter as versões de biaws-api, biaws-ui e biaws-monitor-executor.");
21
+ if (!value.components || Object.keys(value.components).sort().join() !== [...expected].sort().join()) {
22
+ invalid("O manifesto deve conter exatamente os componentes do schema declarado.");
21
23
  }
22
24
  const components = {};
23
- for (const component of COMPONENTS) {
25
+ for (const component of expected) {
24
26
  const version = value.components[component];
25
27
  if (typeof version !== "string" || !VERSION.test(version)) invalid(`Versão inválida: ${component}.`);
26
28
  components[component] = version;
27
29
  }
28
30
  return Object.freeze({
29
- schemaVersion: 1,
31
+ schemaVersion: value.schemaVersion,
30
32
  version: value.version,
31
33
  deploymentRevision: value.deploymentRevision,
32
34
  components: Object.freeze(components),
@@ -55,7 +57,7 @@ export async function readRelease(file, filesystem, { optional = false } = {}) {
55
57
 
56
58
  export async function readAvailableRelease(root, filesystem) {
57
59
  const release = await readRelease(path.join(root, "release.json"), filesystem);
58
- for (const component of COMPONENTS) {
60
+ for (const component of Object.keys(release.components)) {
59
61
  const manifest = JSON.parse(await filesystem.readFile(path.join(root, component, "package.json"), "utf8"));
60
62
  if (manifest.version !== release.components[component]) {
61
63
  invalid(`release.json diverge de ${component}/package.json; atualize o manifesto antes de implantar.`);
@@ -66,7 +68,7 @@ export async function readAvailableRelease(root, filesystem) {
66
68
 
67
69
  export function releaseDifferences(current, available) {
68
70
  const differences = [];
69
- for (const field of ["version", "deploymentRevision", ...COMPONENTS]) {
71
+ for (const field of ["schemaVersion", "version", "deploymentRevision", ...COMPONENTS]) {
70
72
  const before = COMPONENTS.includes(field) ? current?.components[field] : current?.[field];
71
73
  const after = COMPONENTS.includes(field) ? available.components[field] : available[field];
72
74
  if (before !== after) differences.push({ field, current: before ?? null, available: after });
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
+ import { mcpInstanceSummary, mcpPublicConfiguration } from "./mcp.js";
3
4
  import {
4
5
  appliedReleasePath,
5
6
  readAvailableRelease,
@@ -137,6 +138,7 @@ export async function listInstances(context, filesystem) {
137
138
  uiPort: Number(env.BIAWS_UI_PORT || 4_400),
138
139
  publicUrl: env.BIAWS_PUBLIC_URL || `http://localhost:${env.BIAWS_UI_PORT || 4_400}`,
139
140
  storage: Object.values(STORAGE_FIELDS).some((key) => env[key]) ? "directories" : "volumes",
141
+ mcp: mcpInstanceSummary({ env }),
140
142
  env,
141
143
  });
142
144
  }
@@ -238,11 +240,14 @@ export async function buildSetupConfiguration(values, context, filesystem) {
238
240
  const storageChanged = Boolean(
239
241
  previousStorage && Object.keys(STORAGE_FIELDS).some((key) => previousStorage[key] !== storage[key]),
240
242
  );
243
+ const publicUrl = validatePublicUrl(values.publicUrl || existing?.publicUrl || `http://localhost:${ports.uiPort}`);
244
+ if (values.mcpHttp === true || (values.mcpHttp === undefined && existing?.mcp.enabled))
245
+ mcpPublicConfiguration(publicUrl);
241
246
  return Object.freeze({
242
247
  name,
243
248
  ...ports,
244
249
  ...storage,
245
- publicUrl: validatePublicUrl(values.publicUrl || `http://localhost:${ports.uiPort}`),
250
+ publicUrl,
246
251
  storage: values.storage,
247
252
  storageChanged,
248
253
  adminEmail: String(values.adminEmail).trim(),
@@ -257,6 +262,7 @@ export async function buildSetupConfiguration(values, context, filesystem) {
257
262
  apiKeyRateLimitMax: integer(values.apiKeyRateLimitMax, "Rate limit da chave de API"),
258
263
  apiKeyRateLimitWindow: integer(values.apiKeyRateLimitWindow, "Janela da chave de API"),
259
264
  existing: Boolean(existing),
265
+ mcpHttp: values.mcpHttp,
260
266
  });
261
267
  }
262
268
 
@@ -289,6 +295,7 @@ export function setupArguments(configuration, context) {
289
295
  String(configuration.apiKeyRateLimitWindow),
290
296
  ];
291
297
  if (configuration.disableRateLimit) args.push("--disable-rate-limit");
298
+ if (configuration.mcpHttp !== undefined) args.push(configuration.mcpHttp ? "--mcp-http" : "--no-mcp-http");
292
299
  if (configuration.storage === "volumes") args.push("--use-docker-volumes");
293
300
  else {
294
301
  for (const [name, flag] of [
@@ -344,6 +351,10 @@ export function composeArguments(instance, context, operation) {
344
351
  "--project-name",
345
352
  `biaws-${instance.name}`,
346
353
  ];
354
+ if (instance.env?.BIAWS_MCP_CONFIGURED === "true") {
355
+ base.push("--file", path.join(context.repositoryRoot, "compose.mcp.yaml"));
356
+ if (instance.env.BIAWS_MCP_HTTP_ENABLED === "true" || operation === "stop") base.push("--profile", "mcp-http");
357
+ }
347
358
  if (operation === "services") return [...base, "ps", "--all", "--services"];
348
359
  if (operation === "start") return [...base, "up", "-d", "--wait"];
349
360
  if (operation === "update") return [...base, "up", "-d", "--build", "--wait"];
@@ -376,9 +387,11 @@ export async function updateInstance(instance, context, filesystem, processRunne
376
387
  throw new CliError("A release mudou após a verificação; execute update novamente.", { code: "RELEASE_CHANGED" });
377
388
  }
378
389
  const existing = await operateInstance(instance, context, processRunner, "services", { environment });
379
- const services = existing.output.split(/\s+/u).includes("monitor-executor")
380
- ? ["mongo", "api", "ui", "monitor-executor"]
381
- : [];
390
+ const services = ["mongo", "api", "ui"];
391
+ if (existing.output.split(/\s+/u).includes("monitor-executor")) services.push("monitor-executor");
392
+ if (instance.env.BIAWS_MCP_HTTP_ENABLED === "true") services.push("mcp");
393
+ else if (instance.env.BIAWS_MCP_CONFIGURED === "true")
394
+ await operateInstance(instance, context, processRunner, "stop", { services: ["mcp"], environment });
382
395
  const result = await operateInstance(instance, context, processRunner, "update", {
383
396
  environment: { ...environment, BIAWS_VERSION: normalized.version },
384
397
  services,