@bibobonking/opensol 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build.yml +60 -8
- package/CHANGELOG.md +21 -0
- package/README.md +2 -2
- package/desktop/README.md +39 -0
- package/desktop/loading/index.html +36 -0
- package/desktop/src-tauri/Cargo.lock +4301 -0
- package/desktop/src-tauri/Cargo.toml +23 -0
- package/desktop/src-tauri/binaries/.gitkeep +0 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/capabilities/default.json +7 -0
- package/desktop/src-tauri/icons/128x128.png +0 -0
- package/desktop/src-tauri/icons/128x128@2x.png +0 -0
- package/desktop/src-tauri/icons/32x32.png +0 -0
- package/desktop/src-tauri/icons/64x64.png +0 -0
- package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
- package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
- package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
- package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
- package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
- package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
- package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
- package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
- package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
- package/desktop/src-tauri/icons/StoreLogo.png +0 -0
- package/desktop/src-tauri/icons/icon.icns +0 -0
- package/desktop/src-tauri/icons/icon.ico +0 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/lib.rs +210 -0
- package/desktop/src-tauri/src/main.rs +6 -0
- package/desktop/src-tauri/tauri.conf.json +48 -0
- package/dist/assets/index-CeN54A2N.css +1 -0
- package/dist/assets/index-fhIO9a7_.js +318 -0
- package/dist/index.html +4 -4
- package/index.html +2 -2
- package/package.json +2 -1
- package/server/README.md +2 -2
- package/server/daemon.mjs +138 -2
- package/server/lib/mcp-oauth.mjs +219 -0
- package/server/lib/mcp-oauth.test.mjs +175 -0
- package/server/lib/mcp.mjs +899 -0
- package/server/lib/mcp.test.mjs +46 -0
- package/server/lib/store.mjs +22 -0
- package/server/lib/tools.mjs +59 -1
- package/src/App.tsx +2 -0
- package/src/agent/tools.ts +6 -5
- package/src/components/CommandPalette.tsx +14 -1
- package/src/components/Composer.tsx +22 -38
- package/src/components/DiffView.tsx +98 -0
- package/src/components/Logo.tsx +1 -1
- package/src/components/McpPanel.tsx +687 -0
- package/src/components/MessageItem.tsx +62 -13
- package/src/components/ModelDeck.tsx +661 -0
- package/src/components/SessionsTab.tsx +206 -0
- package/src/components/SettingsModal.tsx +146 -71
- package/src/components/Sidebar.tsx +79 -24
- package/src/components/Splash.tsx +1 -1
- package/src/components/ToolCallCard.tsx +29 -7
- package/src/components/WorkspacePanel.tsx +16 -1
- package/src/core/kernel.ts +2 -0
- package/src/core/mcp-tools.ts +63 -0
- package/src/core/plugins/tools/fs.ts +24 -3
- package/src/core/services/agent-loop.ts +3 -1
- package/src/core/services/approval.ts +2 -1
- package/src/core/services/titles.ts +135 -0
- package/src/core/services/tools.ts +11 -8
- package/src/core/types.ts +4 -0
- package/src/data/providers.ts +1 -1
- package/src/index.css +97 -1
- package/src/lib/daemon.ts +38 -0
- package/src/lib/diff.ts +62 -0
- package/src/lib/mcp-config.test.mjs +47 -0
- package/src/lib/mcp-config.ts +203 -0
- package/src/lib/permissions.ts +6 -6
- package/src/store/chats.ts +9 -1
- package/src/store/mcp.ts +94 -0
- package/src/store/settings.ts +10 -7
- package/src/store/ui.ts +17 -0
- package/src/store/workspace.ts +26 -0
- package/src/types.ts +63 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-BmN7pBji.css +0 -1
- package/dist/assets/index-DgNDjkOz.js +0 -301
- package/src/components/ModelPicker.tsx +0 -487
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import http from 'node:http'
|
|
4
|
+
import test from 'node:test'
|
|
5
|
+
import { McpManager, sanitizeServerConfig } from './mcp.mjs'
|
|
6
|
+
|
|
7
|
+
function readBody(req) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let data = ''
|
|
10
|
+
req.on('data', (chunk) => (data += chunk))
|
|
11
|
+
req.on('end', () => resolve(data))
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One fake host: OAuth endpoints, a Streamable HTTP MCP endpoint and a legacy SSE endpoint. */
|
|
16
|
+
async function fakeServer() {
|
|
17
|
+
const seen = { registrations: 0, refreshes: 0, challenge: '', code: 'code-1' }
|
|
18
|
+
const sseClients = []
|
|
19
|
+
let accessToken = 'access-1'
|
|
20
|
+
const rpc = (message) => {
|
|
21
|
+
if (message.method === 'initialize') return { protocolVersion: '2025-03-26', capabilities: {}, serverInfo: { name: 'fake', version: '1' } }
|
|
22
|
+
if (message.method === 'tools/list') return { tools: [{ name: 'hello', description: 'Say hi', inputSchema: { type: 'object', properties: {} } }] }
|
|
23
|
+
if (message.method === 'tools/call') return { content: [{ type: 'text', text: 'hi from fake' }] }
|
|
24
|
+
return {}
|
|
25
|
+
}
|
|
26
|
+
const server = http.createServer(async (req, res) => {
|
|
27
|
+
const base = `http://127.0.0.1:${server.address().port}`
|
|
28
|
+
const url = new URL(req.url, base)
|
|
29
|
+
const send = (status, body, headers = {}) => {
|
|
30
|
+
res.writeHead(status, { 'Content-Type': 'application/json', ...headers })
|
|
31
|
+
res.end(JSON.stringify(body))
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname === '/.well-known/oauth-protected-resource/mcp') return send(200, { resource: `${base}/mcp`, authorization_servers: [base] })
|
|
34
|
+
if (url.pathname === '/.well-known/oauth-authorization-server') {
|
|
35
|
+
return send(200, {
|
|
36
|
+
issuer: base,
|
|
37
|
+
authorization_endpoint: `${base}/authorize`,
|
|
38
|
+
token_endpoint: `${base}/token`,
|
|
39
|
+
registration_endpoint: `${base}/register`,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
if (url.pathname === '/register') {
|
|
43
|
+
seen.registrations += 1
|
|
44
|
+
const body = JSON.parse(await readBody(req))
|
|
45
|
+
assert.equal(body.token_endpoint_auth_method, 'none')
|
|
46
|
+
return send(201, { client_id: 'client-1' })
|
|
47
|
+
}
|
|
48
|
+
if (url.pathname === '/token') {
|
|
49
|
+
const form = new URLSearchParams(await readBody(req))
|
|
50
|
+
if (form.get('grant_type') === 'authorization_code') {
|
|
51
|
+
const expected = createHash('sha256').update(form.get('code_verifier')).digest('base64url')
|
|
52
|
+
if (form.get('code') !== seen.code || expected !== seen.challenge) return send(400, { error: 'invalid_grant' })
|
|
53
|
+
return send(200, { access_token: 'access-1', refresh_token: 'refresh-1', expires_in: 3600 })
|
|
54
|
+
}
|
|
55
|
+
if (form.get('grant_type') === 'refresh_token' && form.get('refresh_token') === 'refresh-1') {
|
|
56
|
+
seen.refreshes += 1
|
|
57
|
+
accessToken = 'access-2'
|
|
58
|
+
return send(200, { access_token: 'access-2', expires_in: 3600 })
|
|
59
|
+
}
|
|
60
|
+
return send(400, { error: 'invalid_grant' })
|
|
61
|
+
}
|
|
62
|
+
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
63
|
+
if (req.headers.authorization !== `Bearer ${accessToken}`) {
|
|
64
|
+
return send(401, { error: 'unauthorized' }, { 'WWW-Authenticate': `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource/mcp"` })
|
|
65
|
+
}
|
|
66
|
+
const message = JSON.parse(await readBody(req))
|
|
67
|
+
if (message.id === undefined) {
|
|
68
|
+
res.writeHead(202)
|
|
69
|
+
return res.end()
|
|
70
|
+
}
|
|
71
|
+
return send(200, { jsonrpc: '2.0', id: message.id, result: rpc(message) })
|
|
72
|
+
}
|
|
73
|
+
if (url.pathname === '/sse' && req.method === 'GET') {
|
|
74
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' })
|
|
75
|
+
res.write('event: endpoint\ndata: /messages?session=1\n\n')
|
|
76
|
+
sseClients.push(res)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (url.pathname === '/messages' && req.method === 'POST') {
|
|
80
|
+
const message = JSON.parse(await readBody(req))
|
|
81
|
+
res.writeHead(202)
|
|
82
|
+
res.end('Accepted')
|
|
83
|
+
if (message.id !== undefined) {
|
|
84
|
+
sseClients.at(-1)?.write(`event: message\ndata: ${JSON.stringify({ jsonrpc: '2.0', id: message.id, result: rpc(message) })}\n\n`)
|
|
85
|
+
}
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
send(404, {})
|
|
89
|
+
})
|
|
90
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
91
|
+
const base = `http://127.0.0.1:${server.address().port}`
|
|
92
|
+
const close = () => {
|
|
93
|
+
for (const client of sseClients) client.end()
|
|
94
|
+
server.closeAllConnections()
|
|
95
|
+
return new Promise((resolve) => server.close(resolve))
|
|
96
|
+
}
|
|
97
|
+
return { base, seen, close, expireAccess: () => (accessToken = 'rotated-away') }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test('OAuth: discovery, dynamic registration, PKCE sign-in and token refresh', async () => {
|
|
101
|
+
const fake = await fakeServer()
|
|
102
|
+
const manager = new McpManager()
|
|
103
|
+
const config = sanitizeServerConfig({ name: 'Secure', transport: 'http', url: `${fake.base}/mcp`, oauth: { enabled: true } })
|
|
104
|
+
await manager.setConfigs([config])
|
|
105
|
+
|
|
106
|
+
let view = await manager.connect(config.id)
|
|
107
|
+
assert.equal(view.status, 'needs_auth')
|
|
108
|
+
assert.equal(view.oauth.authorized, false)
|
|
109
|
+
|
|
110
|
+
const redirectUri = 'http://127.0.0.1:4174/oauth/mcp/callback'
|
|
111
|
+
const { url } = await manager.startAuth(config.id, redirectUri)
|
|
112
|
+
const authorize = new URL(url)
|
|
113
|
+
assert.equal(authorize.pathname, '/authorize')
|
|
114
|
+
assert.equal(authorize.searchParams.get('client_id'), 'client-1')
|
|
115
|
+
assert.equal(authorize.searchParams.get('code_challenge_method'), 'S256')
|
|
116
|
+
assert.equal(authorize.searchParams.get('resource'), `${fake.base}/mcp`)
|
|
117
|
+
fake.seen.challenge = authorize.searchParams.get('code_challenge')
|
|
118
|
+
|
|
119
|
+
await assert.rejects(() => manager.finishAuth('wrong-state', 'code-1'), /expired|already used/)
|
|
120
|
+
await manager.finishAuth(authorize.searchParams.get('state'), 'code-1')
|
|
121
|
+
view = manager.list()[0]
|
|
122
|
+
assert.equal(view.status, 'connected', view.error)
|
|
123
|
+
assert.equal(view.oauth.authorized, true)
|
|
124
|
+
assert.equal(view.tools[0].name, 'hello')
|
|
125
|
+
|
|
126
|
+
// The access token stops working: the manager refreshes once and retries the call.
|
|
127
|
+
fake.expireAccess()
|
|
128
|
+
const result = await manager.callTool(config.id, 'hello', {})
|
|
129
|
+
assert.deepEqual(result, { text: 'hi from fake', isError: false })
|
|
130
|
+
assert.equal(fake.seen.refreshes, 1)
|
|
131
|
+
|
|
132
|
+
// A second sign-in reuses the registered client.
|
|
133
|
+
await manager.startAuth(config.id, redirectUri)
|
|
134
|
+
assert.equal(fake.seen.registrations, 1)
|
|
135
|
+
|
|
136
|
+
await manager.signOut(config.id)
|
|
137
|
+
assert.equal(manager.list()[0].status, 'needs_auth')
|
|
138
|
+
await manager.shutdown()
|
|
139
|
+
await fake.close()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('a 401 without OAuth explains how to fix it', async () => {
|
|
143
|
+
const fake = await fakeServer()
|
|
144
|
+
const manager = new McpManager()
|
|
145
|
+
const config = sanitizeServerConfig({ name: 'Plain', transport: 'http', url: `${fake.base}/mcp` })
|
|
146
|
+
await manager.setConfigs([config])
|
|
147
|
+
const view = await manager.connect(config.id)
|
|
148
|
+
assert.equal(view.status, 'error')
|
|
149
|
+
assert.match(view.error, /401/)
|
|
150
|
+
await fake.close()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('legacy HTTP+SSE servers connect and answer tool calls', async () => {
|
|
154
|
+
const fake = await fakeServer()
|
|
155
|
+
const manager = new McpManager()
|
|
156
|
+
const config = sanitizeServerConfig({ name: 'Legacy', transport: 'sse', url: `${fake.base}/sse` })
|
|
157
|
+
assert.equal(config.transport, 'sse')
|
|
158
|
+
await manager.setConfigs([config])
|
|
159
|
+
const view = await manager.connect(config.id)
|
|
160
|
+
assert.equal(view.status, 'connected', view.error)
|
|
161
|
+
assert.equal(view.tools[0].name, 'hello')
|
|
162
|
+
assert.deepEqual(await manager.callTool(config.id, 'hello', {}), { text: 'hi from fake', isError: false })
|
|
163
|
+
await manager.shutdown()
|
|
164
|
+
await fake.close()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('OAuth client secrets are masked and kept on save', () => {
|
|
168
|
+
const config = sanitizeServerConfig({ name: 'x', transport: 'http', url: 'https://example.com/mcp', oauth: { enabled: true, clientId: 'abc', clientSecret: 's3cret' } })
|
|
169
|
+
const manager = new McpManager()
|
|
170
|
+
manager.configs.set(config.id, config)
|
|
171
|
+
const view = manager.list()[0]
|
|
172
|
+
assert.notEqual(view.oauth.clientSecret, 's3cret')
|
|
173
|
+
const again = sanitizeServerConfig(view, config)
|
|
174
|
+
assert.equal(again.oauth.clientSecret, 's3cret')
|
|
175
|
+
})
|