@robota-sdk/agent-transport 3.0.0-beta.64

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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/headless/index.cjs +1 -0
  3. package/dist/node/headless/index.d.ts +2 -0
  4. package/dist/node/headless/index.js +1 -0
  5. package/dist/node/headless-CWEpJXFK.js +7 -0
  6. package/dist/node/headless-CWEpJXFK.js.map +1 -0
  7. package/dist/node/headless-CsZFelG9.cjs +6 -0
  8. package/dist/node/http/index.cjs +1 -0
  9. package/dist/node/http/index.d.ts +2 -0
  10. package/dist/node/http/index.js +1 -0
  11. package/dist/node/http-CM3TJhrF.cjs +1 -0
  12. package/dist/node/http-DwO1AHG-.js +2 -0
  13. package/dist/node/http-DwO1AHG-.js.map +1 -0
  14. package/dist/node/index--Ti9NzQX.d.ts +64 -0
  15. package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
  16. package/dist/node/index-B_rcr14p.d.ts +47 -0
  17. package/dist/node/index-B_rcr14p.d.ts.map +1 -0
  18. package/dist/node/index-C9LWCL4l.d.ts +34 -0
  19. package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
  20. package/dist/node/index-CAr3ioVh.d.ts +64 -0
  21. package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
  22. package/dist/node/index-CEs25wVk.d.ts +213 -0
  23. package/dist/node/index-CEs25wVk.d.ts.map +1 -0
  24. package/dist/node/index-CvXLpjJO.d.ts +213 -0
  25. package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
  26. package/dist/node/index-D34WUfFH.d.ts +26 -0
  27. package/dist/node/index-D34WUfFH.d.ts.map +1 -0
  28. package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
  29. package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
  30. package/dist/node/index-k3TUjA-T.d.ts +26 -0
  31. package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
  32. package/dist/node/index-nBlMTFkZ.d.ts +34 -0
  33. package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
  34. package/dist/node/index.cjs +1 -0
  35. package/dist/node/index.d.ts +6 -0
  36. package/dist/node/index.js +1 -0
  37. package/dist/node/mcp/index.cjs +1 -0
  38. package/dist/node/mcp/index.d.ts +2 -0
  39. package/dist/node/mcp/index.js +1 -0
  40. package/dist/node/mcp-BXBwF6Wu.js +2 -0
  41. package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
  42. package/dist/node/mcp-DcHuGokt.cjs +1 -0
  43. package/dist/node/tui/index.cjs +1 -0
  44. package/dist/node/tui/index.d.ts +2 -0
  45. package/dist/node/tui/index.js +1 -0
  46. package/dist/node/tui-CeD_6rSo.cjs +24 -0
  47. package/dist/node/tui-zmDTPk4b.js +25 -0
  48. package/dist/node/tui-zmDTPk4b.js.map +1 -0
  49. package/dist/node/ws/index.cjs +1 -0
  50. package/dist/node/ws/index.d.ts +2 -0
  51. package/dist/node/ws/index.js +1 -0
  52. package/dist/node/ws-B-oRccFl.js +2 -0
  53. package/dist/node/ws-B-oRccFl.js.map +1 -0
  54. package/dist/node/ws-COnIgnmn.cjs +1 -0
  55. package/package.json +141 -0
  56. package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
  57. package/src/headless/__tests__/headless-runner.test.ts +484 -0
  58. package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
  59. package/src/headless/__tests__/headless-transport.test.ts +268 -0
  60. package/src/headless/headless-runner.ts +141 -0
  61. package/src/headless/headless-stream-json.ts +142 -0
  62. package/src/headless/headless-transport.ts +43 -0
  63. package/src/headless/index.ts +4 -0
  64. package/src/http/__tests__/http-transport.test.ts +55 -0
  65. package/src/http/__tests__/routes.test.ts +168 -0
  66. package/src/http/http-transport.ts +42 -0
  67. package/src/http/index.ts +4 -0
  68. package/src/http/routes.ts +151 -0
  69. package/src/index.ts +5 -0
  70. package/src/mcp/__tests__/mcp-server.test.ts +66 -0
  71. package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
  72. package/src/mcp/index.ts +4 -0
  73. package/src/mcp/mcp-server.ts +162 -0
  74. package/src/mcp/mcp-transport.ts +48 -0
  75. package/src/tui/App.tsx +478 -0
  76. package/src/tui/BackgroundTaskPanel.tsx +34 -0
  77. package/src/tui/CjkTextInput.tsx +204 -0
  78. package/src/tui/ConfirmPrompt.tsx +69 -0
  79. package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
  80. package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
  81. package/src/tui/InkTerminal.ts +42 -0
  82. package/src/tui/InputArea.tsx +298 -0
  83. package/src/tui/InteractivePrompt.tsx +57 -0
  84. package/src/tui/ListPicker.tsx +94 -0
  85. package/src/tui/MenuSelect.tsx +103 -0
  86. package/src/tui/MessageList.tsx +282 -0
  87. package/src/tui/PermissionPrompt.tsx +84 -0
  88. package/src/tui/PluginTUI.tsx +256 -0
  89. package/src/tui/SessionPicker.tsx +66 -0
  90. package/src/tui/SessionStatusBar.tsx +66 -0
  91. package/src/tui/SlashAutocomplete.tsx +110 -0
  92. package/src/tui/StatusBar.tsx +213 -0
  93. package/src/tui/StreamingIndicator.tsx +91 -0
  94. package/src/tui/TextPrompt.tsx +80 -0
  95. package/src/tui/ToolCommandOutput.tsx +37 -0
  96. package/src/tui/ToolDiffBlock.tsx +30 -0
  97. package/src/tui/TransportTUI.tsx +116 -0
  98. package/src/tui/UpdateNotice.tsx +14 -0
  99. package/src/tui/UsageSummaryEntry.tsx +38 -0
  100. package/src/tui/WaveText.tsx +44 -0
  101. package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
  102. package/src/tui/__tests__/ListPicker.test.tsx +159 -0
  103. package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
  104. package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
  105. package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
  106. package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
  107. package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
  108. package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
  109. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
  110. package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
  111. package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
  112. package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
  113. package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
  114. package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
  115. package/src/tui/__tests__/command-output-summary.test.ts +95 -0
  116. package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
  117. package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
  118. package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
  119. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
  120. package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
  121. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
  122. package/src/tui/__tests__/input-area-flow.test.ts +152 -0
  123. package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
  124. package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
  125. package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
  126. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
  127. package/src/tui/__tests__/render-markdown.test.ts +72 -0
  128. package/src/tui/__tests__/selection-flow.test.ts +61 -0
  129. package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
  130. package/src/tui/__tests__/status-activity.test.ts +71 -0
  131. package/src/tui/__tests__/status-bar.test.tsx +157 -0
  132. package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
  133. package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
  134. package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
  135. package/src/tui/background-task-row-format.ts +52 -0
  136. package/src/tui/command-output-summary.ts +122 -0
  137. package/src/tui/execution-workspace-view-model.ts +123 -0
  138. package/src/tui/flows/cjk-text-input-flow.ts +285 -0
  139. package/src/tui/flows/confirm-prompt-flow.ts +45 -0
  140. package/src/tui/flows/input-area-flow.ts +186 -0
  141. package/src/tui/flows/permission-prompt-flow.ts +76 -0
  142. package/src/tui/flows/selection-flow.ts +126 -0
  143. package/src/tui/flows/text-prompt-flow.ts +98 -0
  144. package/src/tui/hooks/command-effect-handler.ts +98 -0
  145. package/src/tui/hooks/command-effect-queue.ts +39 -0
  146. package/src/tui/hooks/model-change-side-effect.ts +63 -0
  147. package/src/tui/hooks/side-effects-types.ts +38 -0
  148. package/src/tui/hooks/use-interactive-session-init.ts +50 -0
  149. package/src/tui/hooks/useAutocomplete.ts +85 -0
  150. package/src/tui/hooks/useInteractiveSession.ts +273 -0
  151. package/src/tui/hooks/usePermissionQueue.ts +51 -0
  152. package/src/tui/hooks/usePluginCallbacks.ts +30 -0
  153. package/src/tui/hooks/usePluginScreenData.ts +84 -0
  154. package/src/tui/hooks/useSideEffects.ts +210 -0
  155. package/src/tui/hooks/useSlashRouting.ts +117 -0
  156. package/src/tui/hooks/useStatusLineSettings.ts +35 -0
  157. package/src/tui/index.ts +3 -0
  158. package/src/tui/plugin-tui-handlers.ts +163 -0
  159. package/src/tui/render-markdown.ts +129 -0
  160. package/src/tui/render.tsx +60 -0
  161. package/src/tui/status-activity.ts +63 -0
  162. package/src/tui/tui-cli-adapter-context.tsx +12 -0
  163. package/src/tui/tui-cli-adapter.ts +25 -0
  164. package/src/tui/tui-state-manager.ts +225 -0
  165. package/src/tui/tui-transport.ts +32 -0
  166. package/src/tui/types.ts +14 -0
  167. package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
  168. package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
  169. package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
  170. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
  171. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
  172. package/src/tui/utils/edit-diff.ts +152 -0
  173. package/src/tui/utils/paste-labels.ts +9 -0
  174. package/src/tui/utils/tool-call-extractor.ts +91 -0
  175. package/src/tui/utils/tool-diff-summary.ts +75 -0
  176. package/src/ws/__tests__/ws-handler.test.ts +407 -0
  177. package/src/ws/__tests__/ws-transport.test.ts +53 -0
  178. package/src/ws/index.ts +13 -0
  179. package/src/ws/ws-background-messages.ts +170 -0
  180. package/src/ws/ws-handler.ts +279 -0
  181. package/src/ws/ws-protocol.ts +76 -0
  182. package/src/ws/ws-transport-configurable.ts +123 -0
  183. package/src/ws/ws-transport.ts +42 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * HTTP transport adapter — exposes IInteractiveSession over REST API.
3
+ *
4
+ * Built on Hono for Cloudflare Workers + Node.js + AWS Lambda compatibility.
5
+ * Each endpoint maps 1:1 to an IInteractiveSession API method.
6
+ */
7
+
8
+ import { Hono } from 'hono';
9
+ import { streamSSE } from 'hono/streaming';
10
+ import type { Context } from 'hono';
11
+ import type { IInteractiveSession } from '@robota-sdk/agent-framework';
12
+
13
+ /** Callback that resolves an IInteractiveSession from the request context. */
14
+ export type TSessionFactory = (c: Context) => IInteractiveSession | Promise<IInteractiveSession>;
15
+
16
+ export interface IAgentRoutesOptions {
17
+ /** Resolve an IInteractiveSession per request (e.g., by auth token, session ID). */
18
+ sessionFactory: TSessionFactory;
19
+ }
20
+
21
+ /**
22
+ * Create a Hono router with all agent HTTP endpoints.
23
+ *
24
+ * Usage:
25
+ * ```typescript
26
+ * const routes = createAgentRoutes({ sessionFactory });
27
+ * app.route('/agent', routes); // mount on existing app
28
+ * export default routes; // or use standalone (CF Workers)
29
+ * ```
30
+ */
31
+ export function createAgentRoutes(options: IAgentRoutesOptions): Hono {
32
+ const { sessionFactory } = options;
33
+ const app = new Hono();
34
+
35
+ // POST /submit — execute prompt, stream events via SSE
36
+ app.post('/submit', async (c) => {
37
+ const session = await sessionFactory(c);
38
+ const body = await c.req.json<{ prompt: string }>();
39
+
40
+ if (!body.prompt || typeof body.prompt !== 'string') {
41
+ return c.json({ error: 'prompt is required' }, 400);
42
+ }
43
+
44
+ return streamSSE(c, async (stream) => {
45
+ const cleanup: Array<() => void> = [];
46
+
47
+ const subscribe = <T>(event: string, handler: (data: T) => void): void => {
48
+ session.on(event as 'text_delta', handler as () => void);
49
+ cleanup.push(() => session.off(event as 'text_delta', handler as () => void));
50
+ };
51
+
52
+ let completed = false;
53
+ const done = new Promise<void>((resolve) => {
54
+ subscribe('text_delta', (delta: string) => {
55
+ stream.writeSSE({ event: 'text_delta', data: JSON.stringify({ delta }) });
56
+ });
57
+
58
+ subscribe('tool_start', (state) => {
59
+ stream.writeSSE({ event: 'tool_start', data: JSON.stringify(state) });
60
+ });
61
+
62
+ subscribe('tool_end', (state) => {
63
+ stream.writeSSE({ event: 'tool_end', data: JSON.stringify(state) });
64
+ });
65
+
66
+ subscribe('thinking', (isThinking: boolean) => {
67
+ stream.writeSSE({ event: 'thinking', data: JSON.stringify({ isThinking }) });
68
+ if (!isThinking && completed) {
69
+ resolve();
70
+ }
71
+ });
72
+
73
+ subscribe('complete', (result) => {
74
+ completed = true;
75
+ stream.writeSSE({ event: 'complete', data: JSON.stringify(result) });
76
+ });
77
+
78
+ subscribe('interrupted', (result) => {
79
+ completed = true;
80
+ stream.writeSSE({ event: 'interrupted', data: JSON.stringify(result) });
81
+ });
82
+
83
+ subscribe('error', (error: Error) => {
84
+ completed = true;
85
+ stream.writeSSE({ event: 'error', data: JSON.stringify({ message: error.message }) });
86
+ });
87
+ });
88
+
89
+ await session.submit(body.prompt);
90
+ await done;
91
+
92
+ for (const fn of cleanup) fn();
93
+ });
94
+ });
95
+
96
+ // POST /command — execute system command
97
+ app.post('/command', async (c) => {
98
+ const session = await sessionFactory(c);
99
+ const body = await c.req.json<{ name: string; args?: string }>();
100
+
101
+ if (!body.name || typeof body.name !== 'string') {
102
+ return c.json({ error: 'name is required' }, 400);
103
+ }
104
+
105
+ const result = await session.executeCommand(body.name, body.args ?? '');
106
+ if (!result) {
107
+ return c.json({ error: `Unknown command: ${body.name}` }, 404);
108
+ }
109
+ return c.json(result);
110
+ });
111
+
112
+ // POST /abort — abort current execution
113
+ app.post('/abort', async (c) => {
114
+ const session = await sessionFactory(c);
115
+ session.abort();
116
+ return c.json({ ok: true });
117
+ });
118
+
119
+ // POST /cancel-queue — cancel queued prompt
120
+ app.post('/cancel-queue', async (c) => {
121
+ const session = await sessionFactory(c);
122
+ session.cancelQueue();
123
+ return c.json({ ok: true });
124
+ });
125
+
126
+ // GET /messages — get message history
127
+ app.get('/messages', async (c) => {
128
+ const session = await sessionFactory(c);
129
+ return c.json(session.getMessages());
130
+ });
131
+
132
+ // GET /context — get context window state
133
+ app.get('/context', async (c) => {
134
+ const session = await sessionFactory(c);
135
+ return c.json(session.getContextState());
136
+ });
137
+
138
+ // GET /executing — check if currently executing
139
+ app.get('/executing', async (c) => {
140
+ const session = await sessionFactory(c);
141
+ return c.json({ executing: session.isExecuting() });
142
+ });
143
+
144
+ // GET /pending — get pending queued prompt
145
+ app.get('/pending', async (c) => {
146
+ const session = await sessionFactory(c);
147
+ return c.json({ pending: session.getPendingPrompt() });
148
+ });
149
+
150
+ return app;
151
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './tui/index.js';
2
+ export * from './headless/index.js';
3
+ export * from './http/index.js';
4
+ export * from './ws/index.js';
5
+ export * from './mcp/index.js';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tests for MCP transport adapter.
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { createAgentMcpServer } from '../mcp-server.js';
7
+ import type { IInteractiveSession } from '@robota-sdk/agent-framework';
8
+
9
+ function createMockSession(commands?: Array<{ name: string; description: string }>) {
10
+ return {
11
+ submit: vi.fn(),
12
+ abort: vi.fn(),
13
+ cancelQueue: vi.fn(),
14
+ getMessages: vi.fn().mockReturnValue([]),
15
+ getContextState: vi
16
+ .fn()
17
+ .mockReturnValue({ usedTokens: 0, maxTokens: 200000, usedPercentage: 0 }),
18
+ isExecuting: vi.fn().mockReturnValue(false),
19
+ getPendingPrompt: vi.fn().mockReturnValue(null),
20
+ executeCommand: vi.fn().mockResolvedValue({ message: 'done', success: true }),
21
+ listCommands: vi.fn().mockReturnValue(
22
+ commands ?? [
23
+ { name: 'clear', description: 'Clear history' },
24
+ { name: 'mode', description: 'Permission mode' },
25
+ ],
26
+ ),
27
+ on: vi.fn(),
28
+ off: vi.fn(),
29
+ } as unknown as IInteractiveSession;
30
+ }
31
+
32
+ describe('createAgentMcpServer', () => {
33
+ it('creates a server instance', () => {
34
+ const server = createAgentMcpServer({
35
+ name: 'test-agent',
36
+ version: '1.0.0',
37
+ session: createMockSession(),
38
+ });
39
+ expect(server).toBeDefined();
40
+ });
41
+
42
+ it('exposeCommands=false does not call listCommands', () => {
43
+ const session = createMockSession();
44
+ createAgentMcpServer({
45
+ name: 'test',
46
+ version: '1.0.0',
47
+ session,
48
+ exposeCommands: false,
49
+ });
50
+ expect(session.listCommands).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('exposeCommands=true calls session.listCommands()', () => {
54
+ const session = createMockSession([
55
+ { name: 'clear', description: 'Clear' },
56
+ { name: 'help', description: 'Help' },
57
+ ]);
58
+ createAgentMcpServer({
59
+ name: 'test',
60
+ version: '1.0.0',
61
+ session,
62
+ exposeCommands: true,
63
+ });
64
+ expect(session.listCommands).toHaveBeenCalled();
65
+ });
66
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createMcpTransport } from '../mcp-transport.js';
3
+ import type { IInteractiveSession } from '@robota-sdk/agent-framework';
4
+
5
+ function createMockSession(): IInteractiveSession {
6
+ return {
7
+ submit: vi.fn(),
8
+ abort: vi.fn(),
9
+ cancelQueue: vi.fn(),
10
+ getMessages: vi.fn().mockReturnValue([]),
11
+ getContextState: vi
12
+ .fn()
13
+ .mockReturnValue({ usedPercentage: 0, usedTokens: 0, maxTokens: 200000 }),
14
+ isExecuting: vi.fn().mockReturnValue(false),
15
+ getPendingPrompt: vi.fn().mockReturnValue(null),
16
+ executeCommand: vi.fn().mockResolvedValue({ message: 'ok', success: true }),
17
+ listCommands: vi.fn().mockReturnValue([]),
18
+ on: vi.fn(),
19
+ off: vi.fn(),
20
+ } as unknown as IInteractiveSession;
21
+ }
22
+
23
+ describe('createMcpTransport', () => {
24
+ it('returns an adapter with name "mcp"', () => {
25
+ const transport = createMcpTransport({ name: 'test', version: '1.0.0' });
26
+ expect(transport.name).toBe('mcp');
27
+ });
28
+
29
+ it('throws if start() is called without attach()', async () => {
30
+ const transport = createMcpTransport({ name: 'test', version: '1.0.0' });
31
+ await expect(transport.start()).rejects.toThrow('No session attached');
32
+ });
33
+
34
+ it('throws if getServer() is called before start()', () => {
35
+ const transport = createMcpTransport({ name: 'test', version: '1.0.0' });
36
+ expect(() => transport.getServer()).toThrow('Transport not started');
37
+ });
38
+
39
+ it('creates an MCP server after attach + start', async () => {
40
+ const transport = createMcpTransport({ name: 'test', version: '1.0.0' });
41
+ transport.attach(createMockSession() as never);
42
+ await transport.start();
43
+ const server = transport.getServer();
44
+ expect(server).toBeDefined();
45
+ });
46
+ });
@@ -0,0 +1,4 @@
1
+ export { createAgentMcpServer } from './mcp-server.js';
2
+ export type { IAgentMcpOptions } from './mcp-server.js';
3
+ export { createMcpTransport } from './mcp-transport.js';
4
+ export type { IMcpTransportOptions } from './mcp-transport.js';
@@ -0,0 +1,162 @@
1
+ /**
2
+ * MCP transport adapter — exposes IInteractiveSession as an MCP server.
3
+ *
4
+ * Uses the low-level MCP Server class to avoid TypeScript depth issues
5
+ * with McpServer.registerTool() generics. Registers tools/list and
6
+ * tools/call handlers directly.
7
+ */
8
+
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
11
+ import type { IInteractiveSession, IExecutionResult } from '@robota-sdk/agent-framework';
12
+
13
+ export interface IAgentMcpOptions {
14
+ /** Name for the MCP server. */
15
+ name: string;
16
+ /** Version string. */
17
+ version: string;
18
+ /** IInteractiveSession to expose. */
19
+ session: IInteractiveSession;
20
+ /** If true, register each system command as a separate MCP tool. Default: true. */
21
+ exposeCommands?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Create an MCP server that exposes IInteractiveSession over Model Context Protocol.
26
+ *
27
+ * Usage:
28
+ * ```typescript
29
+ * const server = createAgentMcpServer({
30
+ * name: 'robota-agent',
31
+ * version: '1.0.0',
32
+ * session: interactiveSession,
33
+ * });
34
+ *
35
+ * import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
36
+ * await server.connect(new StdioServerTransport());
37
+ * ```
38
+ */
39
+ export function createAgentMcpServer(options: IAgentMcpOptions): Server {
40
+ const { name, version, session, exposeCommands = true } = options;
41
+
42
+ const server = new Server({ name, version }, { capabilities: { tools: {} } });
43
+
44
+ // Build tool definitions
45
+ const tools: Array<{
46
+ name: string;
47
+ description: string;
48
+ inputSchema: Record<string, unknown>;
49
+ }> = [
50
+ {
51
+ name: 'submit',
52
+ description: 'Submit a prompt to the AI agent and wait for the response',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ prompt: { type: 'string', description: 'The prompt to send to the agent' },
57
+ },
58
+ required: ['prompt'],
59
+ },
60
+ },
61
+ ];
62
+
63
+ if (exposeCommands) {
64
+ for (const cmd of session.listCommands()) {
65
+ tools.push({
66
+ name: `command_${cmd.name}`,
67
+ description: cmd.description,
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ args: { type: 'string', description: 'Command arguments' },
72
+ },
73
+ },
74
+ });
75
+ }
76
+ }
77
+
78
+ // tools/list handler
79
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
80
+ tools,
81
+ }));
82
+
83
+ // tools/call handler
84
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
85
+ const { name: toolName, arguments: toolArgs } = request.params;
86
+
87
+ if (toolName === 'submit') {
88
+ const prompt = (toolArgs as Record<string, string>)?.prompt;
89
+ if (!prompt) {
90
+ return {
91
+ content: [{ type: 'text', text: 'Error: prompt is required' }],
92
+ isError: true,
93
+ };
94
+ }
95
+ const result = await waitForCompletion(session, prompt);
96
+ return {
97
+ content: [{ type: 'text', text: result.response }],
98
+ };
99
+ }
100
+
101
+ // System commands: command_<name>
102
+ if (toolName.startsWith('command_')) {
103
+ const cmdName = toolName.slice('command_'.length);
104
+ const args = (toolArgs as Record<string, string>)?.args ?? '';
105
+ const result = await session.executeCommand(cmdName, args);
106
+ return {
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: result?.message ?? `Unknown command: ${cmdName}`,
111
+ },
112
+ ],
113
+ isError: !result,
114
+ };
115
+ }
116
+
117
+ return {
118
+ content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
119
+ isError: true,
120
+ };
121
+ });
122
+
123
+ return server;
124
+ }
125
+
126
+ /**
127
+ * Submit a prompt and wait for the complete/interrupted/error event.
128
+ */
129
+ function waitForCompletion(
130
+ session: IInteractiveSession,
131
+ prompt: string,
132
+ ): Promise<IExecutionResult> {
133
+ return new Promise((resolve, reject) => {
134
+ const onComplete = (result: IExecutionResult): void => {
135
+ cleanup();
136
+ resolve(result);
137
+ };
138
+ const onInterrupted = (result: IExecutionResult): void => {
139
+ cleanup();
140
+ resolve(result);
141
+ };
142
+ const onError = (error: Error): void => {
143
+ cleanup();
144
+ reject(error);
145
+ };
146
+
147
+ const cleanup = (): void => {
148
+ session.off('complete', onComplete);
149
+ session.off('interrupted', onInterrupted);
150
+ session.off('error', onError);
151
+ };
152
+
153
+ session.on('complete', onComplete);
154
+ session.on('interrupted', onInterrupted);
155
+ session.on('error', onError);
156
+
157
+ session.submit(prompt).catch((err) => {
158
+ cleanup();
159
+ reject(err instanceof Error ? err : new Error(String(err)));
160
+ });
161
+ });
162
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ITransportAdapter implementation for MCP transport.
3
+ *
4
+ * Wraps createAgentMcpServer into the unified ITransportAdapter interface
5
+ * while exposing the underlying MCP Server via getServer().
6
+ */
7
+
8
+ import type { ITransportAdapter } from '@robota-sdk/agent-interface-transport';
9
+ import type { IInteractiveSession } from '@robota-sdk/agent-framework';
10
+ import { createAgentMcpServer } from './mcp-server.js';
11
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+
13
+ export interface IMcpTransportOptions {
14
+ /** Name for the MCP server. */
15
+ name: string;
16
+ /** Version string. */
17
+ version: string;
18
+ /** If true, register each system command as a separate MCP tool. Default: true. */
19
+ exposeCommands?: boolean;
20
+ }
21
+
22
+ export function createMcpTransport(
23
+ options: IMcpTransportOptions,
24
+ ): ITransportAdapter<IInteractiveSession> & { getServer(): Server } {
25
+ let session: IInteractiveSession | null = null;
26
+ let server: Server | null = null;
27
+
28
+ return {
29
+ name: 'mcp',
30
+ attach(s: IInteractiveSession) {
31
+ session = s;
32
+ },
33
+ async start() {
34
+ if (!session) throw new Error('No session attached. Call attach() first.');
35
+ server = createAgentMcpServer({ ...options, session });
36
+ },
37
+ async stop() {
38
+ if (server) {
39
+ await server.close();
40
+ server = null;
41
+ }
42
+ },
43
+ getServer() {
44
+ if (!server) throw new Error('Transport not started. Call start() first.');
45
+ return server;
46
+ },
47
+ };
48
+ }