maka-agent 0.2.0-dev.45.20260921 → 0.2.0-dev.47.20260922

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.
Files changed (53) hide show
  1. package/DISCLAIMER-WIP +6 -1
  2. package/README.md +6 -1
  3. package/dist/acp/maka-acp-agent.js +1 -1
  4. package/dist/acp/session-mcp.js +212 -0
  5. package/dist/acp/session-registry.js +52 -8
  6. package/dist/acp/stdio-server.js +3 -0
  7. package/dist/cli-core.js +1 -1
  8. package/dist/mcp-capability-provider.js +6 -1
  9. package/dist/mcp-capability-publication.js +139 -0
  10. package/dist/pi-tui-layout.js +46 -17
  11. package/dist/pi-tui-pickers.js +25 -14
  12. package/dist/pi-tui-runner.js +24 -15
  13. package/dist/runtime-host-service-manager.js +26 -2
  14. package/dist/tui-context-refresh.js +15 -26
  15. package/dist/tui-mcp-control.js +29 -88
  16. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  17. package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
  18. package/node_modules/@maka/core/dist/model-thinking.js +36 -1
  19. package/node_modules/@maka/core/dist/provider-registry.js +0 -1
  20. package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
  21. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
  22. package/node_modules/@maka/core/dist/session.js +17 -1
  23. package/node_modules/@maka/core/dist/settings.js +2 -0
  24. package/node_modules/@maka/core/package.json +1 -0
  25. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
  26. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
  27. package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
  28. package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
  29. package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
  30. package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
  31. package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
  32. package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
  33. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +6 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +15 -2
  35. package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
  36. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
  37. package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
  38. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
  39. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
  40. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
  41. package/node_modules/@maka/runtime-host/dist/protocol/index.js +11 -1
  42. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +35 -3
  43. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
  44. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
  45. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
  46. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +29 -2
  47. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
  48. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
  49. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +1 -1
  50. package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
  51. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +32 -4
  52. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
  53. package/package.json +1 -1
package/DISCLAIMER-WIP CHANGED
@@ -11,7 +11,12 @@ policy. For example, releases may have incomplete or un-reviewed licensing
11
11
  conditions. What follows is a list of issues the project is currently aware of
12
12
  (this list is likely to be incomplete):
13
13
 
14
- * The software grant and committer ICLAs are not yet complete.
14
+ * The initial-code authorization review remains open at
15
+ https://github.com/apache/maka/issues/3268. Since RC1, the ASF has recorded
16
+ the ICLA of contributor Mingqwqqaq, and the Deep Research implementation
17
+ has been removed. The review still needs to record the authorization basis
18
+ for the retained initial code; these changes do not establish that an SGA
19
+ has been filed.
15
20
 
16
21
  If you are planning to incorporate this work into your product/project, please
17
22
  be aware that you will need to conduct a thorough licensing review to determine
package/README.md CHANGED
@@ -47,7 +47,12 @@ policy. For example, releases may have incomplete or un-reviewed licensing
47
47
  conditions. What follows is a list of issues the project is currently aware of
48
48
  (this list is likely to be incomplete):
49
49
 
50
- * The software grant and committer ICLAs are not yet complete.
50
+ * The initial-code authorization review remains open at
51
+ https://github.com/apache/maka/issues/3268. Since RC1, the ASF has recorded
52
+ the ICLA of contributor Mingqwqqaq, and the Deep Research implementation
53
+ has been removed. The review still needs to record the authorization basis
54
+ for the retained initial code; these changes do not establish that an SGA
55
+ has been filed.
51
56
 
52
57
  If you are planning to incorporate this work into your product/project, please
53
58
  be aware that you will need to conduct a thorough licensing review to determine
@@ -29,7 +29,7 @@ export function createMakaAcpAgent(options) {
29
29
  agentInfo: { name: 'maka', title: 'Maka', version: options.version },
30
30
  };
31
31
  })
32
- .onRequest(methods.agent.session.new, ({ params }) => options.sessionRegistry.create(params))
32
+ .onRequest(methods.agent.session.new, ({ params, signal }) => options.sessionRegistry.create(params, signal))
33
33
  .onRequest(methods.agent.session.list, ({ params }) => options.sessionRegistry.list(params))
34
34
  .onRequest(methods.agent.session.setConfigOption, ({ params }) => options.sessionRegistry.setConfigOption(params))
35
35
  .onRequest(methods.agent.session.prompt, ({ params, signal, client }) => options.sessionRegistry.prompt(params, {
@@ -0,0 +1,212 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { isAbsolute } from 'node:path';
20
+ import { RequestError } from '@agentclientprotocol/sdk';
21
+ import { MCP_CONFIG_VERSION } from '@maka/core/mcp';
22
+ import { McpClientManager } from '@maka/mcp';
23
+ import { normalizeMcpConfig } from '@maka/storage/mcp-config-store';
24
+ import { abortable } from '@maka/runtime-host/client';
25
+ import { createMcpCapabilityProvider } from '../mcp-capability-provider.js';
26
+ import { McpCapabilityPublication } from '../mcp-capability-publication.js';
27
+ /** Validates before any process or Host work; never writes a user MCP configuration. */
28
+ export function createAcpMcpConfig(params) {
29
+ const servers = Object.create(null);
30
+ if (!Array.isArray(params.mcpServers))
31
+ throw invalidMcpInput('invalid_servers');
32
+ for (const server of params.mcpServers) {
33
+ if (!server || typeof server !== 'object' || 'type' in server || !('command' in server)) {
34
+ throw invalidMcpInput('unsupported_transport');
35
+ }
36
+ if (typeof server.name !== 'string' || Object.hasOwn(servers, server.name)) {
37
+ throw invalidMcpInput('duplicate_or_invalid_name');
38
+ }
39
+ if (typeof server.command !== 'string' ||
40
+ !isAbsolute(server.command) ||
41
+ server.command.includes('\0')) {
42
+ throw invalidMcpInput('command_must_be_absolute');
43
+ }
44
+ if (!Array.isArray(server.args) ||
45
+ server.args.some((arg) => typeof arg !== 'string' || arg.includes('\0'))) {
46
+ throw invalidMcpInput('invalid_arguments');
47
+ }
48
+ if (!Array.isArray(server.env))
49
+ throw invalidMcpInput('invalid_environment');
50
+ const env = Object.create(null);
51
+ for (const variable of server.env) {
52
+ if (!variable ||
53
+ typeof variable.name !== 'string' ||
54
+ variable.name.length === 0 ||
55
+ variable.name.includes('\0') ||
56
+ variable.name.includes('=') ||
57
+ Object.hasOwn(env, variable.name) ||
58
+ typeof variable.value !== 'string' ||
59
+ variable.value.includes('\0')) {
60
+ throw invalidMcpInput('duplicate_or_invalid_environment');
61
+ }
62
+ env[variable.name] = variable.value;
63
+ }
64
+ servers[server.name] = {
65
+ command: server.command,
66
+ args: server.args,
67
+ env,
68
+ cwd: params.cwd,
69
+ protocol: 'auto',
70
+ };
71
+ }
72
+ try {
73
+ return normalizeMcpConfig({ version: MCP_CONFIG_VERSION, mcpServers: servers });
74
+ }
75
+ catch {
76
+ throw invalidMcpInput('invalid_configuration');
77
+ }
78
+ }
79
+ /** Owns one Session's in-memory MCP processes and publication on the shared Host connection. */
80
+ export class AcpSessionMcp {
81
+ #sessionId;
82
+ #config;
83
+ #manager = new McpClientManager({
84
+ clientName: 'maka-acp',
85
+ excludedStdioEnvironmentKeys: ['MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL'],
86
+ });
87
+ #publication;
88
+ #unsubscribeManager;
89
+ #unsubscribeConnection;
90
+ #availability;
91
+ #prepared = false;
92
+ #closed = false;
93
+ #authoritativelyRetired = false;
94
+ #closeTask;
95
+ constructor(sessionId, config, connection) {
96
+ this.#sessionId = sessionId;
97
+ this.#config = config;
98
+ this.#publication = new McpCapabilityPublication({
99
+ connectionIdentity: () => this.#availability?.kind === 'connected'
100
+ ? this.#availability.hostEpoch + '\0' + this.#availability.connectionId
101
+ : undefined,
102
+ revision: () => this.#manager.toolSnapshot().revision,
103
+ createProvider: () => createMcpCapabilityProvider(this.#manager, {
104
+ admission: 'mcp',
105
+ onCurrentRegistrationRetired: () => this.#retire(),
106
+ }) ?? {
107
+ // Empty is still an authoritative Session snapshot. Publish it on a
108
+ // new connection to clear lost Host contracts, and retain retirement
109
+ // notification even while every configured server has no tools.
110
+ offers: () => [],
111
+ currentRegistrationRetired: () => this.#retire(),
112
+ },
113
+ replace: (provider) => connection.replaceClientCapabilities(provider, { sessionId }),
114
+ unregister: () => connection.unregisterClientCapabilities({ sessionId }),
115
+ onState: () => undefined,
116
+ });
117
+ this.#unsubscribeManager = this.#manager.onChange(() => {
118
+ if (this.#prepared && !this.#closed)
119
+ this.#publication.request();
120
+ });
121
+ this.#unsubscribeConnection = connection.subscribeConnectionAvailability((availability) => {
122
+ this.#availability = availability;
123
+ if (availability.kind !== 'connected')
124
+ this.#publication.invalidate();
125
+ if (this.#prepared && !this.#closed)
126
+ this.#publication.request();
127
+ });
128
+ }
129
+ async prepare(signal) {
130
+ signal?.throwIfAborted();
131
+ const cancel = () => {
132
+ void this.close().catch(() => undefined);
133
+ };
134
+ signal?.addEventListener('abort', cancel, { once: true });
135
+ try {
136
+ await this.#manager.sync(this.#config);
137
+ signal?.throwIfAborted();
138
+ this.#assertConnected();
139
+ this.#prepared = true;
140
+ await this.ready(signal);
141
+ this.#assertConnected();
142
+ }
143
+ catch (error) {
144
+ await this.close().catch(() => undefined);
145
+ if (error instanceof RequestError)
146
+ throw error;
147
+ throw mcpUnavailable(this.#sessionId, 'mcp.prepare', 'mcp_preparation_failed');
148
+ }
149
+ finally {
150
+ signal?.removeEventListener('abort', cancel);
151
+ }
152
+ }
153
+ async ready(signal) {
154
+ signal?.throwIfAborted();
155
+ this.#assertOpen('mcp.ready');
156
+ const state = await abortable(() => this.#publication.settle(), signal);
157
+ signal?.throwIfAborted();
158
+ this.#assertOpen('mcp.ready');
159
+ if (state !== 'published' && state !== 'not_published') {
160
+ throw mcpUnavailable(this.#sessionId, 'mcp.ready', 'mcp_publication_failed');
161
+ }
162
+ }
163
+ close() {
164
+ return this.#close(true);
165
+ }
166
+ #retire() {
167
+ this.#authoritativelyRetired = true;
168
+ return this.#close(false);
169
+ }
170
+ #close(unregister) {
171
+ if (this.#closeTask)
172
+ return this.#closeTask;
173
+ this.#closed = true;
174
+ this.#unsubscribeManager();
175
+ this.#unsubscribeConnection();
176
+ const managerClose = this.#manager.close();
177
+ this.#closeTask = (async () => {
178
+ try {
179
+ try {
180
+ await (unregister ? this.#publication.close() : this.#publication.retire());
181
+ }
182
+ catch (error) {
183
+ // Session retirement is authoritative. If it wins the race with a local
184
+ // unregister, the obsolete unregister may reject even though withdrawal
185
+ // has already completed on the Host.
186
+ if (!this.#authoritativelyRetired)
187
+ throw error;
188
+ }
189
+ }
190
+ finally {
191
+ await managerClose;
192
+ }
193
+ })();
194
+ return this.#closeTask;
195
+ }
196
+ #assertConnected() {
197
+ this.#assertOpen('mcp.prepare');
198
+ if (Object.keys(this.#config.mcpServers).some((serverId) => this.#manager.status(serverId)?.state !== 'connected')) {
199
+ throw mcpUnavailable(this.#sessionId, 'mcp.prepare', 'mcp_not_ready');
200
+ }
201
+ }
202
+ #assertOpen(operation) {
203
+ if (this.#closed)
204
+ throw mcpUnavailable(this.#sessionId, operation, 'mcp_not_ready');
205
+ }
206
+ }
207
+ function invalidMcpInput(reason) {
208
+ return RequestError.invalidParams({ field: 'mcpServers', reason }, 'Invalid ACP stdio MCP configuration');
209
+ }
210
+ function mcpUnavailable(sessionId, operation, code) {
211
+ return RequestError.internalError({ source: 'adapter', operation, sessionId, code }, 'Session MCP tools are unavailable');
212
+ }
@@ -28,6 +28,7 @@ import { RuntimeHostSessionUpdateError, getRuntimeHostSession, requireRuntimeHos
28
28
  import { AcpSessionConfigInputError, createAcpSessionConfigPatch, projectAcpSessionConfigOptions, validateAcpSessionConfigOptionRequest, } from './session-configuration.js';
29
29
  import { AcpSessionEventMapper } from './session-event-mapper.js';
30
30
  import { mapAcpPromptContent, publishAcpPromptAttachments } from './prompt-content.js';
31
+ import { AcpSessionMcp, createAcpMcpConfig } from './session-mcp.js';
31
32
  import { AcpSessionInteractions } from './session-interactions.js';
32
33
  const ACP_SESSION_CURSOR_MAX_BYTES = 8 * 1024;
33
34
  const ADMISSION_QUERY_MAX_ATTEMPTS = 5;
@@ -40,6 +41,8 @@ export class AcpSessionRegistry {
40
41
  #newTurnId;
41
42
  #inFlightOperations = new Set();
42
43
  #ownedSessionIds = new Set();
44
+ #mcps = new Map();
45
+ #creationAbort = new AbortController();
43
46
  #attachmentInteractions = new Map();
44
47
  #attachments = new Map();
45
48
  #attachmentOpenControllers = new Map();
@@ -58,10 +61,11 @@ export class AcpSessionRegistry {
58
61
  this.#newSessionId = options.newSessionId ?? randomUUID;
59
62
  this.#newTurnId = options.newTurnId ?? randomUUID;
60
63
  }
61
- async create(params) {
64
+ async create(params, signal) {
62
65
  this.#assertOpen('session.create');
63
66
  validateNewSessionParams(params);
64
- return this.#track(this.#create(params));
67
+ const mcpConfig = createAcpMcpConfig(params);
68
+ return this.#track(this.#create(params, mcpConfig, signal));
65
69
  }
66
70
  async list(params) {
67
71
  this.#assertOpen('session.catalog.query');
@@ -130,6 +134,7 @@ export class AcpSessionRegistry {
130
134
  dispose() {
131
135
  this.#closing = true;
132
136
  this.#connectAbortController?.abort();
137
+ this.#creationAbort.abort();
133
138
  this.#disposeTask ??= this.#dispose();
134
139
  return this.#disposeTask;
135
140
  }
@@ -214,6 +219,9 @@ export class AcpSessionRegistry {
214
219
  throw error;
215
220
  throw requestErrorFromRuntimeHost(error, 'artifact.ingest');
216
221
  }
222
+ if (active.cancelled)
223
+ return { stopReason: await this.#cancelledStopReason(active) };
224
+ await this.#mcps.get(params.sessionId)?.ready(active.projectionAbort.signal);
217
225
  if (active.cancelled)
218
226
  return { stopReason: await this.#cancelledStopReason(active) };
219
227
  active.transcript = attachment.trackPromptTranscript(turnId);
@@ -591,8 +599,8 @@ export class AcpSessionRegistry {
591
599
  onInteractionPending: (pending) => {
592
600
  if (!interactions.fencesTurn(pending.turnId) &&
593
601
  ![...(this.#activePrompts.get(sessionId) ?? [])].some((active) => active.turnId === pending.turnId && active.dispatchStarted)) {
594
- // Idle attachments may observe a Turn started by another client. Do not
595
- // present its interaction through this ACP connection's client.
602
+ // An idle attachment can observe another client's Turn; it does not
603
+ // transfer that Turn's interaction authority to this ACP client.
596
604
  return;
597
605
  }
598
606
  void interactions.pending(pending);
@@ -697,6 +705,14 @@ export class AcpSessionRegistry {
697
705
  closeError = error;
698
706
  }
699
707
  }
708
+ const mcp = this.#mcps.get(sessionId);
709
+ this.#mcps.delete(sessionId);
710
+ try {
711
+ await mcp?.close();
712
+ }
713
+ catch (error) {
714
+ closeError ??= error;
715
+ }
700
716
  await delivery;
701
717
  const failedCancellation = cancellation.find((result) => result.status === 'rejected');
702
718
  if (failedCancellation)
@@ -745,11 +761,25 @@ export class AcpSessionRegistry {
745
761
  if (!this.#ownedSessionIds.has(sessionId))
746
762
  throw unknownSessionError();
747
763
  }
748
- async #create(params) {
764
+ async #create(params, mcpConfig, signal) {
765
+ const lifetime = signal
766
+ ? AbortSignal.any([signal, this.#creationAbort.signal])
767
+ : this.#creationAbort.signal;
768
+ lifetime.throwIfAborted();
749
769
  const connection = await this.#getConnection('session.create');
750
770
  const sessionId = this.#newSessionId();
771
+ let mcp;
772
+ if (params.mcpServers.length > 0) {
773
+ mcp = new AcpSessionMcp(sessionId, mcpConfig, connection);
774
+ this.#mcps.set(sessionId, mcp);
775
+ }
751
776
  let result;
777
+ let dispatched = false;
752
778
  try {
779
+ await mcp?.prepare(lifetime);
780
+ lifetime.throwIfAborted();
781
+ this.#assertOpen('session.create');
782
+ dispatched = true;
753
783
  result = await connection.request('session.create', {
754
784
  sessionId,
755
785
  workspace: { kind: 'host_path', path: params.cwd },
@@ -757,6 +787,20 @@ export class AcpSessionRegistry {
757
787
  });
758
788
  }
759
789
  catch (error) {
790
+ const outcomeUnknown = dispatched &&
791
+ error instanceof RuntimeHostRequestInterruptedError &&
792
+ error.dispatch === 'dispatched';
793
+ if (outcomeUnknown && !this.#closing) {
794
+ // The error returns this ID. Keep its connection-local reservation usable
795
+ // without guessing whether Host committed or resending Session creation.
796
+ this.#ownedSessionIds.add(sessionId);
797
+ }
798
+ else {
799
+ this.#mcps.delete(sessionId);
800
+ await mcp?.close().catch(() => undefined);
801
+ }
802
+ if (error instanceof RequestError)
803
+ throw error;
760
804
  throw requestErrorFromRuntimeHost(error, 'session.create', { sessionId });
761
805
  }
762
806
  // Session creation has committed. Optional presentation failures must not
@@ -911,6 +955,9 @@ export class AcpSessionRegistry {
911
955
  }
912
956
  }
913
957
  await Promise.allSettled(cancellations);
958
+ const mcps = [...this.#mcps.values()];
959
+ this.#mcps.clear();
960
+ await Promise.allSettled(mcps.map((mcp) => mcp.close()));
914
961
  await Promise.allSettled([this.#closeOwnedConnection()]);
915
962
  await Promise.allSettled([
916
963
  ...this.#inFlightOperations,
@@ -995,9 +1042,6 @@ function registryClosedError(operation) {
995
1042
  }
996
1043
  function validateNewSessionParams(params) {
997
1044
  assertBoundedAbsoluteCwd(params.cwd);
998
- if (params.mcpServers.length > 0) {
999
- throw RequestError.invalidParams({ field: 'mcpServers', reason: 'unsupported' }, 'MCP servers are not supported by this ACP adapter yet');
1000
- }
1001
1045
  if ((params.additionalDirectories?.length ?? 0) > 0) {
1002
1046
  throw RequestError.invalidParams({ field: 'additionalDirectories', reason: 'unsupported' }, 'Additional directories are not supported by this ACP adapter yet');
1003
1047
  }
@@ -40,6 +40,9 @@ export async function runMakaAcpStdioServer(input, dependencies = {}) {
40
40
  request: connection.request.bind(connection),
41
41
  openSessionSubscription: connection.openSessionSubscription.bind(connection),
42
42
  openSessionSubscriptionOnce: connection.openSessionSubscriptionOnce.bind(connection),
43
+ replaceClientCapabilities: (provider, options) => connection.replaceClientCapabilities(provider, options),
44
+ unregisterClientCapabilities: (options) => connection.unregisterClientCapabilities(options),
45
+ subscribeConnectionAvailability: (listener) => connection.subscribeConnectionAvailability(listener),
43
46
  close: () => context.close(),
44
47
  };
45
48
  },
package/dist/cli-core.js CHANGED
@@ -101,7 +101,7 @@ function helpText(cliCommand) {
101
101
  'Commands:',
102
102
  ...[
103
103
  ['', 'Start the TUI'],
104
- ['--acp', 'Serve ACP v1 over stdio (sessions, prompts, streaming, cancellation)'],
104
+ ['--acp', 'Serve ACP v1 over stdio (sessions, tools, permissions, forms, stdio MCP)'],
105
105
  ['run ...', 'Run one non-interactive model turn'],
106
106
  ['-p ...', `Alias for ${cliCommand} run`],
107
107
  ['activate ...', 'Run one Cloud Session activation and emit JSONL'],
@@ -20,7 +20,7 @@ import { createHash } from 'node:crypto';
20
20
  import { clientCapabilityEntityId } from '@maka/runtime-host/client-capability-entity-id';
21
21
  import { CLIENT_CAPABILITY_MAX_TOOLS, CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER, decodeClientCapabilityReplaceInput, } from '@maka/runtime-host/protocol';
22
22
  const CAPABILITY_VERSION = '0';
23
- export function createMcpCapabilityProvider(manager) {
23
+ export function createMcpCapabilityProvider(manager, options = {}) {
24
24
  const toolSnapshot = manager.toolSnapshot();
25
25
  const tools = [...toolSnapshot.tools].sort((left, right) => left.descriptor.serverId.localeCompare(right.descriptor.serverId) ||
26
26
  left.descriptor.name.localeCompare(right.descriptor.name));
@@ -52,6 +52,7 @@ export function createMcpCapabilityProvider(manager) {
52
52
  version: CAPABILITY_VERSION,
53
53
  affinity: 'session',
54
54
  hostPathAccess: 'none',
55
+ ...(options.admission ? { admission: options.admission } : {}),
55
56
  label: servers.size === 1
56
57
  ? `MCP: ${chunk[0]?.source.descriptor.serverId ?? 'tools'}`.slice(0, 128)
57
58
  : `MCP tools (${servers.size} servers)`,
@@ -64,10 +65,14 @@ export function createMcpCapabilityProvider(manager) {
64
65
  }
65
66
  const canonical = decodeClientCapabilityReplaceInput({
66
67
  registrationId: '00000000-0000-4000-8000-000000000000',
68
+ ...(options.admission ? { sessionId: 'mcp-manifest-validation' } : {}),
67
69
  offers,
68
70
  });
69
71
  return {
70
72
  offers: () => canonical.offers,
73
+ ...(options.onCurrentRegistrationRetired
74
+ ? { currentRegistrationRetired: options.onCurrentRegistrationRetired }
75
+ : {}),
71
76
  call: async (frame, options) => {
72
77
  const binding = bindings.get(capabilityBindingKey(frame.offerId, frame.serverId, frame.toolName));
73
78
  if (!binding)
@@ -0,0 +1,139 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /** Coalesces MCP snapshots and never commits a publication from an obsolete connection. */
20
+ export class McpCapabilityPublication {
21
+ #options;
22
+ #requested = false;
23
+ #task;
24
+ #closeTask;
25
+ #closed = false;
26
+ #state = 'unavailable';
27
+ #published;
28
+ constructor(options) {
29
+ this.#options = options;
30
+ }
31
+ invalidate() {
32
+ this.#published = undefined;
33
+ }
34
+ request() {
35
+ if (this.#closed)
36
+ return;
37
+ this.#requested = true;
38
+ if (this.#task)
39
+ return;
40
+ this.#task = this.#run().finally(() => {
41
+ this.#task = undefined;
42
+ if (this.#requested && !this.#closed)
43
+ this.request();
44
+ });
45
+ }
46
+ async settle() {
47
+ this.request();
48
+ while (this.#task)
49
+ await this.#task;
50
+ return this.#closed ? 'unavailable' : this.#state;
51
+ }
52
+ close() {
53
+ this.#closeTask ??= this.#close(true);
54
+ return this.#closeTask;
55
+ }
56
+ /** Stops local publication state after the Host has already retired its current registration. */
57
+ retire() {
58
+ this.#closeTask ??= this.#close(false);
59
+ return this.#closeTask;
60
+ }
61
+ async #close(unregister) {
62
+ this.#closed = true;
63
+ this.#requested = false;
64
+ await this.#task;
65
+ try {
66
+ if (unregister &&
67
+ this.#published?.registered &&
68
+ this.#published.identity === this.#options.connectionIdentity()) {
69
+ await this.#options.unregister();
70
+ }
71
+ }
72
+ finally {
73
+ this.#published = undefined;
74
+ }
75
+ }
76
+ async #run() {
77
+ while (this.#requested && !this.#closed) {
78
+ this.#requested = false;
79
+ await this.#publish();
80
+ }
81
+ }
82
+ async #publish() {
83
+ const identity = this.#options.connectionIdentity();
84
+ if (identity === undefined) {
85
+ this.#setState('unavailable');
86
+ return;
87
+ }
88
+ const revision = this.#options.revision();
89
+ if (this.#published?.identity === identity && this.#published.revision === revision) {
90
+ this.#setState(this.#published.registered ? 'published' : 'not_published');
91
+ return;
92
+ }
93
+ let provider;
94
+ this.#setState('publishing');
95
+ try {
96
+ provider = this.#options.createProvider();
97
+ if (provider)
98
+ await this.#options.replace(provider);
99
+ else if (this.#published?.identity === identity && this.#published.registered) {
100
+ await this.#options.unregister();
101
+ }
102
+ }
103
+ catch {
104
+ try {
105
+ await provider?.close?.();
106
+ }
107
+ catch {
108
+ /* Rejected provider cleanup is best effort. */
109
+ }
110
+ if (this.#isCurrent(identity, revision))
111
+ this.#setState('error');
112
+ else if (!this.#closed)
113
+ this.#requested = true;
114
+ return;
115
+ }
116
+ // Even an obsolete snapshot may have committed on the current connection.
117
+ // Retain that fact so an empty replacement or close can unregister it.
118
+ if (this.#options.connectionIdentity() === identity) {
119
+ this.#published = { identity, revision, registered: provider !== undefined };
120
+ }
121
+ if (!this.#isCurrent(identity, revision)) {
122
+ if (!this.#closed)
123
+ this.#requested = true;
124
+ return;
125
+ }
126
+ this.#setState(provider ? 'published' : 'not_published');
127
+ }
128
+ #isCurrent(identity, revision) {
129
+ return (!this.#closed &&
130
+ this.#options.connectionIdentity() === identity &&
131
+ this.#options.revision() === revision);
132
+ }
133
+ #setState(state) {
134
+ if (this.#closed)
135
+ return;
136
+ this.#state = state;
137
+ this.#options.onState(state);
138
+ }
139
+ }