@codeany/open-agent-sdk 0.1.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.
Files changed (57) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +388 -0
  4. package/examples/01-simple-query.ts +43 -0
  5. package/examples/02-multi-tool.ts +44 -0
  6. package/examples/03-multi-turn.ts +39 -0
  7. package/examples/04-prompt-api.ts +29 -0
  8. package/examples/05-custom-system-prompt.ts +26 -0
  9. package/examples/06-mcp-server.ts +49 -0
  10. package/examples/07-custom-tools.ts +87 -0
  11. package/examples/08-official-api-compat.ts +38 -0
  12. package/examples/09-subagents.ts +48 -0
  13. package/examples/10-permissions.ts +40 -0
  14. package/examples/11-custom-mcp-tools.ts +101 -0
  15. package/examples/web/index.html +365 -0
  16. package/examples/web/server.ts +157 -0
  17. package/package.json +60 -0
  18. package/src/agent.ts +425 -0
  19. package/src/engine.ts +520 -0
  20. package/src/hooks.ts +261 -0
  21. package/src/index.ts +376 -0
  22. package/src/mcp/client.ts +150 -0
  23. package/src/sdk-mcp-server.ts +78 -0
  24. package/src/session.ts +227 -0
  25. package/src/tool-helper.ts +127 -0
  26. package/src/tools/agent-tool.ts +153 -0
  27. package/src/tools/ask-user.ts +79 -0
  28. package/src/tools/bash.ts +75 -0
  29. package/src/tools/config-tool.ts +89 -0
  30. package/src/tools/cron-tools.ts +153 -0
  31. package/src/tools/edit.ts +74 -0
  32. package/src/tools/glob.ts +77 -0
  33. package/src/tools/grep.ts +168 -0
  34. package/src/tools/index.ts +232 -0
  35. package/src/tools/lsp-tool.ts +163 -0
  36. package/src/tools/mcp-resource-tools.ts +125 -0
  37. package/src/tools/notebook-edit.ts +93 -0
  38. package/src/tools/plan-tools.ts +88 -0
  39. package/src/tools/read.ts +73 -0
  40. package/src/tools/send-message.ts +96 -0
  41. package/src/tools/task-tools.ts +290 -0
  42. package/src/tools/team-tools.ts +128 -0
  43. package/src/tools/todo-tool.ts +112 -0
  44. package/src/tools/tool-search.ts +87 -0
  45. package/src/tools/types.ts +62 -0
  46. package/src/tools/web-fetch.ts +66 -0
  47. package/src/tools/web-search.ts +86 -0
  48. package/src/tools/worktree-tools.ts +140 -0
  49. package/src/tools/write.ts +42 -0
  50. package/src/types.ts +459 -0
  51. package/src/utils/compact.ts +206 -0
  52. package/src/utils/context.ts +191 -0
  53. package/src/utils/fileCache.ts +148 -0
  54. package/src/utils/messages.ts +196 -0
  55. package/src/utils/retry.ts +140 -0
  56. package/src/utils/tokens.ts +122 -0
  57. package/tsconfig.json +19 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * GrepTool - Search file contents using regex
3
+ */
4
+
5
+ import { spawn } from 'child_process'
6
+ import { resolve } from 'path'
7
+ import { defineTool } from './types.js'
8
+
9
+ export const GrepTool = defineTool({
10
+ name: 'Grep',
11
+ description: 'Search file contents using regex patterns. Uses ripgrep (rg) if available, falls back to grep. Supports file type filtering and context lines.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ pattern: {
16
+ type: 'string',
17
+ description: 'The regex pattern to search for',
18
+ },
19
+ path: {
20
+ type: 'string',
21
+ description: 'File or directory to search in (defaults to cwd)',
22
+ },
23
+ glob: {
24
+ type: 'string',
25
+ description: 'Glob pattern to filter files (e.g., "*.ts", "*.{js,jsx}")',
26
+ },
27
+ type: {
28
+ type: 'string',
29
+ description: 'File type filter (e.g., "ts", "py", "js")',
30
+ },
31
+ output_mode: {
32
+ type: 'string',
33
+ enum: ['content', 'files_with_matches', 'count'],
34
+ description: 'Output mode (default: files_with_matches)',
35
+ },
36
+ '-i': {
37
+ type: 'boolean',
38
+ description: 'Case insensitive search',
39
+ },
40
+ '-n': {
41
+ type: 'boolean',
42
+ description: 'Show line numbers (default: true)',
43
+ },
44
+ '-A': { type: 'number', description: 'Lines after match' },
45
+ '-B': { type: 'number', description: 'Lines before match' },
46
+ '-C': { type: 'number', description: 'Context lines' },
47
+ context: { type: 'number', description: 'Context lines (alias for -C)' },
48
+ head_limit: { type: 'number', description: 'Limit output entries (default: 250)' },
49
+ },
50
+ required: ['pattern'],
51
+ },
52
+ isReadOnly: true,
53
+ isConcurrencySafe: true,
54
+ async call(input, context) {
55
+ const searchPath = input.path ? resolve(context.cwd, input.path) : context.cwd
56
+ const outputMode = input.output_mode || 'files_with_matches'
57
+ const headLimit = input.head_limit ?? 250
58
+
59
+ // Build rg command (fall back to grep if rg unavailable)
60
+ const args: string[] = []
61
+
62
+ // Try ripgrep first
63
+ let cmd = 'rg'
64
+
65
+ if (outputMode === 'files_with_matches') {
66
+ args.push('--files-with-matches')
67
+ } else if (outputMode === 'count') {
68
+ args.push('--count')
69
+ } else {
70
+ // content mode
71
+ if (input['-n'] !== false) args.push('--line-number')
72
+ }
73
+
74
+ if (input['-i']) args.push('--ignore-case')
75
+ if (input['-A']) args.push('-A', String(input['-A']))
76
+ if (input['-B']) args.push('-B', String(input['-B']))
77
+ const ctx = input['-C'] ?? input.context
78
+ if (ctx) args.push('-C', String(ctx))
79
+ if (input.glob) args.push('--glob', input.glob)
80
+ if (input.type) args.push('--type', input.type)
81
+
82
+ args.push('--', input.pattern, searchPath)
83
+
84
+ return new Promise<string>((resolvePromise) => {
85
+ const proc = spawn(cmd, args, {
86
+ cwd: context.cwd,
87
+ timeout: 30000,
88
+ })
89
+
90
+ const chunks: Buffer[] = []
91
+ const errChunks: Buffer[] = []
92
+ proc.stdout?.on('data', (d: Buffer) => chunks.push(d))
93
+ proc.stderr?.on('data', (d: Buffer) => errChunks.push(d))
94
+
95
+ proc.on('close', (code) => {
96
+ let result = Buffer.concat(chunks).toString('utf-8').trim()
97
+
98
+ if (!result && code !== 0) {
99
+ // Try fallback to grep
100
+ const grepArgs = ['-r']
101
+ if (input['-i']) grepArgs.push('-i')
102
+ if (outputMode === 'files_with_matches') grepArgs.push('-l')
103
+ if (outputMode === 'count') grepArgs.push('-c')
104
+ if (outputMode === 'content' && input['-n'] !== false) grepArgs.push('-n')
105
+ if (input.glob) grepArgs.push('--include', input.glob)
106
+ grepArgs.push('--', input.pattern, searchPath)
107
+
108
+ const grepProc = spawn('grep', grepArgs, {
109
+ cwd: context.cwd,
110
+ timeout: 30000,
111
+ })
112
+
113
+ const grepChunks: Buffer[] = []
114
+ grepProc.stdout?.on('data', (d: Buffer) => grepChunks.push(d))
115
+ grepProc.on('close', () => {
116
+ const grepResult = Buffer.concat(grepChunks).toString('utf-8').trim()
117
+ if (!grepResult) {
118
+ resolvePromise(`No matches found for pattern "${input.pattern}"`)
119
+ } else {
120
+ // Apply head limit
121
+ const lines = grepResult.split('\n')
122
+ if (headLimit > 0 && lines.length > headLimit) {
123
+ resolvePromise(lines.slice(0, headLimit).join('\n') + `\n... (${lines.length - headLimit} more)`)
124
+ } else {
125
+ resolvePromise(grepResult)
126
+ }
127
+ }
128
+ })
129
+ grepProc.on('error', () => {
130
+ resolvePromise(`No matches found for pattern "${input.pattern}"`)
131
+ })
132
+ return
133
+ }
134
+
135
+ if (!result) {
136
+ resolvePromise(`No matches found for pattern "${input.pattern}"`)
137
+ return
138
+ }
139
+
140
+ // Apply head limit
141
+ const lines = result.split('\n')
142
+ if (headLimit > 0 && lines.length > headLimit) {
143
+ result = lines.slice(0, headLimit).join('\n') + `\n... (${lines.length - headLimit} more)`
144
+ }
145
+
146
+ resolvePromise(result)
147
+ })
148
+
149
+ proc.on('error', () => {
150
+ // rg not found, try grep directly
151
+ const grepArgs = ['-r', '-n', '--', input.pattern, searchPath]
152
+ const grepProc = spawn('grep', grepArgs, {
153
+ cwd: context.cwd,
154
+ timeout: 30000,
155
+ })
156
+ const grepChunks: Buffer[] = []
157
+ grepProc.stdout?.on('data', (d: Buffer) => grepChunks.push(d))
158
+ grepProc.on('close', () => {
159
+ const grepResult = Buffer.concat(grepChunks).toString('utf-8').trim()
160
+ resolvePromise(grepResult || `No matches found for pattern "${input.pattern}"`)
161
+ })
162
+ grepProc.on('error', () => {
163
+ resolvePromise(`Error: neither rg nor grep available`)
164
+ })
165
+ })
166
+ })
167
+ },
168
+ })
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Tool Registry - All built-in tool definitions
3
+ *
4
+ * 30+ tools covering file I/O, execution, search, web, agents,
5
+ * tasks, teams, messaging, worktree, planning, scheduling, and more.
6
+ */
7
+
8
+ import type { ToolDefinition } from '../types.js'
9
+
10
+ // File I/O
11
+ import { BashTool } from './bash.js'
12
+ import { FileReadTool } from './read.js'
13
+ import { FileWriteTool } from './write.js'
14
+ import { FileEditTool } from './edit.js'
15
+ import { GlobTool } from './glob.js'
16
+ import { GrepTool } from './grep.js'
17
+ import { NotebookEditTool } from './notebook-edit.js'
18
+
19
+ // Web
20
+ import { WebFetchTool } from './web-fetch.js'
21
+ import { WebSearchTool } from './web-search.js'
22
+
23
+ // Agent & Multi-agent
24
+ import { AgentTool } from './agent-tool.js'
25
+ import { SendMessageTool } from './send-message.js'
26
+ import { TeamCreateTool, TeamDeleteTool } from './team-tools.js'
27
+
28
+ // Tasks
29
+ import {
30
+ TaskCreateTool,
31
+ TaskListTool,
32
+ TaskUpdateTool,
33
+ TaskGetTool,
34
+ TaskStopTool,
35
+ TaskOutputTool,
36
+ } from './task-tools.js'
37
+
38
+ // Worktree
39
+ import { EnterWorktreeTool, ExitWorktreeTool } from './worktree-tools.js'
40
+
41
+ // Planning
42
+ import { EnterPlanModeTool, ExitPlanModeTool } from './plan-tools.js'
43
+
44
+ // User interaction
45
+ import { AskUserQuestionTool } from './ask-user.js'
46
+
47
+ // Discovery
48
+ import { ToolSearchTool } from './tool-search.js'
49
+
50
+ // MCP Resources
51
+ import { ListMcpResourcesTool, ReadMcpResourceTool } from './mcp-resource-tools.js'
52
+
53
+ // Scheduling
54
+ import { CronCreateTool, CronDeleteTool, CronListTool, RemoteTriggerTool } from './cron-tools.js'
55
+
56
+ // LSP
57
+ import { LSPTool } from './lsp-tool.js'
58
+
59
+ // Config
60
+ import { ConfigTool } from './config-tool.js'
61
+
62
+ // Todo
63
+ import { TodoWriteTool } from './todo-tool.js'
64
+
65
+ /**
66
+ * All built-in tools (30+).
67
+ */
68
+ const ALL_TOOLS: ToolDefinition[] = [
69
+ // Core file I/O & execution
70
+ BashTool,
71
+ FileReadTool,
72
+ FileWriteTool,
73
+ FileEditTool,
74
+ GlobTool,
75
+ GrepTool,
76
+ NotebookEditTool,
77
+
78
+ // Web
79
+ WebFetchTool,
80
+ WebSearchTool,
81
+
82
+ // Agent & Multi-agent
83
+ AgentTool,
84
+ SendMessageTool,
85
+ TeamCreateTool,
86
+ TeamDeleteTool,
87
+
88
+ // Tasks
89
+ TaskCreateTool,
90
+ TaskListTool,
91
+ TaskUpdateTool,
92
+ TaskGetTool,
93
+ TaskStopTool,
94
+ TaskOutputTool,
95
+
96
+ // Worktree
97
+ EnterWorktreeTool,
98
+ ExitWorktreeTool,
99
+
100
+ // Planning
101
+ EnterPlanModeTool,
102
+ ExitPlanModeTool,
103
+
104
+ // User interaction
105
+ AskUserQuestionTool,
106
+
107
+ // Discovery
108
+ ToolSearchTool,
109
+
110
+ // MCP Resources
111
+ ListMcpResourcesTool,
112
+ ReadMcpResourceTool,
113
+
114
+ // Scheduling
115
+ CronCreateTool,
116
+ CronDeleteTool,
117
+ CronListTool,
118
+ RemoteTriggerTool,
119
+
120
+ // LSP
121
+ LSPTool,
122
+
123
+ // Config
124
+ ConfigTool,
125
+
126
+ // Todo
127
+ TodoWriteTool,
128
+ ]
129
+
130
+ /**
131
+ * Get all built-in tools.
132
+ */
133
+ export function getAllBaseTools(): ToolDefinition[] {
134
+ return [...ALL_TOOLS]
135
+ }
136
+
137
+ /**
138
+ * Filter tools by allowed/disallowed lists.
139
+ */
140
+ export function filterTools(
141
+ tools: ToolDefinition[],
142
+ allowedTools?: string[],
143
+ disallowedTools?: string[],
144
+ ): ToolDefinition[] {
145
+ let filtered = tools
146
+
147
+ if (allowedTools && allowedTools.length > 0) {
148
+ const allowed = new Set(allowedTools)
149
+ filtered = filtered.filter((t) => allowed.has(t.name))
150
+ }
151
+
152
+ if (disallowedTools && disallowedTools.length > 0) {
153
+ const disallowed = new Set(disallowedTools)
154
+ filtered = filtered.filter((t) => !disallowed.has(t.name))
155
+ }
156
+
157
+ return filtered
158
+ }
159
+
160
+ /**
161
+ * Assemble tool pool: base tools + MCP tools, with deduplication.
162
+ */
163
+ export function assembleToolPool(
164
+ baseTools: ToolDefinition[],
165
+ mcpTools: ToolDefinition[] = [],
166
+ allowedTools?: string[],
167
+ disallowedTools?: string[],
168
+ ): ToolDefinition[] {
169
+ const combined = [...baseTools, ...mcpTools]
170
+
171
+ // Deduplicate by name (later definitions override)
172
+ const byName = new Map<string, ToolDefinition>()
173
+ for (const tool of combined) {
174
+ byName.set(tool.name, tool)
175
+ }
176
+
177
+ let tools = Array.from(byName.values())
178
+ return filterTools(tools, allowedTools, disallowedTools)
179
+ }
180
+
181
+ // Re-export individual tools
182
+ export {
183
+ // Core
184
+ BashTool,
185
+ FileReadTool,
186
+ FileWriteTool,
187
+ FileEditTool,
188
+ GlobTool,
189
+ GrepTool,
190
+ NotebookEditTool,
191
+ WebFetchTool,
192
+ WebSearchTool,
193
+ // Agent
194
+ AgentTool,
195
+ SendMessageTool,
196
+ TeamCreateTool,
197
+ TeamDeleteTool,
198
+ // Tasks
199
+ TaskCreateTool,
200
+ TaskListTool,
201
+ TaskUpdateTool,
202
+ TaskGetTool,
203
+ TaskStopTool,
204
+ TaskOutputTool,
205
+ // Worktree
206
+ EnterWorktreeTool,
207
+ ExitWorktreeTool,
208
+ // Planning
209
+ EnterPlanModeTool,
210
+ ExitPlanModeTool,
211
+ // User
212
+ AskUserQuestionTool,
213
+ // Discovery
214
+ ToolSearchTool,
215
+ // MCP
216
+ ListMcpResourcesTool,
217
+ ReadMcpResourceTool,
218
+ // Scheduling
219
+ CronCreateTool,
220
+ CronDeleteTool,
221
+ CronListTool,
222
+ RemoteTriggerTool,
223
+ // LSP
224
+ LSPTool,
225
+ // Config
226
+ ConfigTool,
227
+ // Todo
228
+ TodoWriteTool,
229
+ }
230
+
231
+ // Re-export helpers
232
+ export { defineTool, toApiTool } from './types.js'
@@ -0,0 +1,163 @@
1
+ /**
2
+ * LSPTool - Language Server Protocol integration
3
+ *
4
+ * Provides code intelligence: go-to-definition, find-references,
5
+ * hover, document symbols, workspace symbols, etc.
6
+ */
7
+
8
+ import { execSync } from 'child_process'
9
+ import type { ToolDefinition, ToolResult } from '../types.js'
10
+
11
+ export const LSPTool: ToolDefinition = {
12
+ name: 'LSP',
13
+ description: 'Language Server Protocol operations for code intelligence. Supports go-to-definition, find-references, hover, and symbol lookup.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ operation: {
18
+ type: 'string',
19
+ enum: [
20
+ 'goToDefinition',
21
+ 'findReferences',
22
+ 'hover',
23
+ 'documentSymbol',
24
+ 'workspaceSymbol',
25
+ 'goToImplementation',
26
+ 'prepareCallHierarchy',
27
+ 'incomingCalls',
28
+ 'outgoingCalls',
29
+ ],
30
+ description: 'LSP operation to perform',
31
+ },
32
+ file_path: { type: 'string', description: 'File path for the operation' },
33
+ line: { type: 'number', description: 'Line number (0-based)' },
34
+ character: { type: 'number', description: 'Character position (0-based)' },
35
+ query: { type: 'string', description: 'Symbol name (for workspace symbol search)' },
36
+ },
37
+ required: ['operation'],
38
+ },
39
+ isReadOnly: () => true,
40
+ isConcurrencySafe: () => true,
41
+ isEnabled: () => true,
42
+ async prompt() { return 'Code intelligence via Language Server Protocol.' },
43
+ async call(input: any, context: { cwd: string }): Promise<ToolResult> {
44
+ const { operation, file_path, line, character, query } = input
45
+
46
+ // LSP requires a running language server. In standalone mode,
47
+ // we fall back to basic grep/ripgrep-based symbol lookup.
48
+ try {
49
+ switch (operation) {
50
+ case 'goToDefinition':
51
+ case 'goToImplementation': {
52
+ if (!file_path || line === undefined) {
53
+ return { type: 'tool_result', tool_use_id: '', content: 'file_path and line required', is_error: true }
54
+ }
55
+ // Use grep to find definition
56
+ const symbol = await getSymbolAtPosition(file_path, line, character || 0, context.cwd)
57
+ if (!symbol) {
58
+ return { type: 'tool_result', tool_use_id: '', content: 'Could not identify symbol at position' }
59
+ }
60
+ const results = execSync(
61
+ `rg -n "(?:function|class|interface|type|const|let|var|export)\\s+${symbol}" --type-add 'src:*.{ts,tsx,js,jsx,py,go,rs,java}' -t src ${context.cwd} 2>/dev/null || grep -rn "(?:function|class|interface|type|const|let|var|export)\\s*${symbol}" ${context.cwd} --include='*.ts' --include='*.js' 2>/dev/null`,
62
+ { encoding: 'utf-8', timeout: 10000 },
63
+ ).trim()
64
+ return { type: 'tool_result', tool_use_id: '', content: results || `No definition found for "${symbol}"` }
65
+ }
66
+
67
+ case 'findReferences': {
68
+ if (!file_path || line === undefined) {
69
+ return { type: 'tool_result', tool_use_id: '', content: 'file_path and line required', is_error: true }
70
+ }
71
+ const sym = await getSymbolAtPosition(file_path, line, character || 0, context.cwd)
72
+ if (!sym) {
73
+ return { type: 'tool_result', tool_use_id: '', content: 'Could not identify symbol at position' }
74
+ }
75
+ const refs = execSync(
76
+ `rg -n "${sym}" ${context.cwd} --type-add 'src:*.{ts,tsx,js,jsx,py,go,rs,java}' -t src 2>/dev/null | head -50`,
77
+ { encoding: 'utf-8', timeout: 10000 },
78
+ ).trim()
79
+ return { type: 'tool_result', tool_use_id: '', content: refs || `No references found for "${sym}"` }
80
+ }
81
+
82
+ case 'hover': {
83
+ return {
84
+ type: 'tool_result',
85
+ tool_use_id: '',
86
+ content: 'Hover information requires a running language server. Use Read tool to examine the file content.',
87
+ }
88
+ }
89
+
90
+ case 'documentSymbol': {
91
+ if (!file_path) {
92
+ return { type: 'tool_result', tool_use_id: '', content: 'file_path required', is_error: true }
93
+ }
94
+ const symbols = execSync(
95
+ `rg -n "^\\s*(export\\s+)?(function|class|interface|type|const|let|var|enum)\\s+" ${JSON.stringify(file_path)} 2>/dev/null || grep -n "^\\s*\\(export\\s\\+\\)\\?\\(function\\|class\\|interface\\|type\\|const\\|let\\|var\\|enum\\)\\s" ${JSON.stringify(file_path)} 2>/dev/null`,
96
+ { encoding: 'utf-8', cwd: context.cwd, timeout: 10000 },
97
+ ).trim()
98
+ return { type: 'tool_result', tool_use_id: '', content: symbols || 'No symbols found' }
99
+ }
100
+
101
+ case 'workspaceSymbol': {
102
+ if (!query) {
103
+ return { type: 'tool_result', tool_use_id: '', content: 'query required', is_error: true }
104
+ }
105
+ const wsSymbols = execSync(
106
+ `rg -n "${query}" ${context.cwd} --type-add 'src:*.{ts,tsx,js,jsx,py,go,rs,java}' -t src 2>/dev/null | head -30`,
107
+ { encoding: 'utf-8', timeout: 10000 },
108
+ ).trim()
109
+ return { type: 'tool_result', tool_use_id: '', content: wsSymbols || `No symbols found for "${query}"` }
110
+ }
111
+
112
+ default:
113
+ return {
114
+ type: 'tool_result',
115
+ tool_use_id: '',
116
+ content: `LSP operation "${operation}" requires a running language server.`,
117
+ }
118
+ }
119
+ } catch (err: any) {
120
+ return {
121
+ type: 'tool_result',
122
+ tool_use_id: '',
123
+ content: `LSP error: ${err.message}`,
124
+ is_error: true,
125
+ }
126
+ }
127
+ },
128
+ }
129
+
130
+ /**
131
+ * Get the symbol at a given position in a file.
132
+ */
133
+ async function getSymbolAtPosition(
134
+ filePath: string,
135
+ line: number,
136
+ character: number,
137
+ cwd: string,
138
+ ): Promise<string | null> {
139
+ try {
140
+ const { readFile } = await import('fs/promises')
141
+ const { resolve } = await import('path')
142
+ const content = await readFile(resolve(cwd, filePath), 'utf-8')
143
+ const lines = content.split('\n')
144
+
145
+ if (line >= lines.length) return null
146
+
147
+ const lineText = lines[line]
148
+ if (!lineText || character >= lineText.length) return null
149
+
150
+ // Extract word at position
151
+ const wordMatch = /\b\w+\b/g
152
+ let match
153
+ while ((match = wordMatch.exec(lineText)) !== null) {
154
+ if (match.index <= character && match.index + match[0].length >= character) {
155
+ return match[0]
156
+ }
157
+ }
158
+
159
+ return null
160
+ } catch {
161
+ return null
162
+ }
163
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * MCP Resource Tools
3
+ *
4
+ * ListMcpResources / ReadMcpResource - Access resources from MCP servers.
5
+ */
6
+
7
+ import type { ToolDefinition, ToolResult } from '../types.js'
8
+ import type { MCPConnection } from '../mcp/client.js'
9
+
10
+ // Registry of MCP connections (set by the agent)
11
+ let mcpConnections: MCPConnection[] = []
12
+
13
+ /**
14
+ * Set MCP connections for resource access.
15
+ */
16
+ export function setMcpConnections(connections: MCPConnection[]): void {
17
+ mcpConnections = connections
18
+ }
19
+
20
+ export const ListMcpResourcesTool: ToolDefinition = {
21
+ name: 'ListMcpResources',
22
+ description: 'List available resources from connected MCP servers. Resources can include files, databases, and other data sources.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ server: { type: 'string', description: 'Filter by MCP server name' },
27
+ },
28
+ },
29
+ isReadOnly: () => true,
30
+ isConcurrencySafe: () => true,
31
+ isEnabled: () => true,
32
+ async prompt() { return 'List MCP resources.' },
33
+ async call(input: any): Promise<ToolResult> {
34
+ const connections = input.server
35
+ ? mcpConnections.filter(c => c.name === input.server)
36
+ : mcpConnections
37
+
38
+ if (connections.length === 0) {
39
+ return {
40
+ type: 'tool_result',
41
+ tool_use_id: '',
42
+ content: 'No MCP servers connected.',
43
+ }
44
+ }
45
+
46
+ const results: string[] = []
47
+
48
+ for (const conn of connections) {
49
+ if (conn.status !== 'connected') continue
50
+
51
+ try {
52
+ // Access the underlying client to list resources
53
+ const resources = (conn as any)._client?.listResources?.()
54
+ if (resources) {
55
+ results.push(`Server: ${conn.name}`)
56
+ for (const r of resources) {
57
+ results.push(` - ${r.name}: ${r.description || r.uri || ''}`)
58
+ }
59
+ } else {
60
+ results.push(`Server: ${conn.name} (${conn.tools.length} tools available)`)
61
+ }
62
+ } catch {
63
+ results.push(`Server: ${conn.name} (resource listing not supported)`)
64
+ }
65
+ }
66
+
67
+ return {
68
+ type: 'tool_result',
69
+ tool_use_id: '',
70
+ content: results.join('\n') || 'No resources found.',
71
+ }
72
+ },
73
+ }
74
+
75
+ export const ReadMcpResourceTool: ToolDefinition = {
76
+ name: 'ReadMcpResource',
77
+ description: 'Read a specific resource from an MCP server.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ server: { type: 'string', description: 'MCP server name' },
82
+ uri: { type: 'string', description: 'Resource URI to read' },
83
+ },
84
+ required: ['server', 'uri'],
85
+ },
86
+ isReadOnly: () => true,
87
+ isConcurrencySafe: () => true,
88
+ isEnabled: () => true,
89
+ async prompt() { return 'Read an MCP resource.' },
90
+ async call(input: any): Promise<ToolResult> {
91
+ const conn = mcpConnections.find(c => c.name === input.server)
92
+ if (!conn) {
93
+ return {
94
+ type: 'tool_result',
95
+ tool_use_id: '',
96
+ content: `MCP server not found: ${input.server}`,
97
+ is_error: true,
98
+ }
99
+ }
100
+
101
+ try {
102
+ const result = await (conn as any)._client?.readResource?.({ uri: input.uri })
103
+ if (result?.contents) {
104
+ const texts = result.contents.map((c: any) => c.text || JSON.stringify(c)).join('\n')
105
+ return {
106
+ type: 'tool_result',
107
+ tool_use_id: '',
108
+ content: texts,
109
+ }
110
+ }
111
+ return {
112
+ type: 'tool_result',
113
+ tool_use_id: '',
114
+ content: 'Resource read returned no content.',
115
+ }
116
+ } catch (err: any) {
117
+ return {
118
+ type: 'tool_result',
119
+ tool_use_id: '',
120
+ content: `Error reading resource: ${err.message}`,
121
+ is_error: true,
122
+ }
123
+ }
124
+ },
125
+ }