Compare commits

...

13 Commits

Author SHA1 Message Date
68db65c61b merge & fix tsx lanuch issue 2025-06-04 00:17:26 +08:00
li1553770945
167c791452 fix:修复依赖问题 2025-06-03 22:24:20 +08:00
Li Yaning
547d07c8fa
Merge pull request #29 from LSTM-Kirigaya/feat/auth
完成了Oauth认证
2025-06-03 21:51:43 +08:00
Li Yaning
740293ab74
Merge branch 'main' into feat/auth 2025-06-03 21:51:26 +08:00
li1553770945
fe2a7c68cb bugfix:修复配置文件不符合要求导致整个插件崩溃的bug 2025-06-03 21:24:50 +08:00
li1553770945
46115bcfba feat:更新导入方式 2025-05-30 00:26:11 +08:00
li1553770945
ae7eb45403 feat:更新导入方式 2025-05-30 00:20:58 +08:00
Li Yaning
dd4331cdaa
Merge pull request #23 from LSTM-Kirigaya/main
merge main
2025-05-30 00:02:08 +08:00
li1553770945
054017bfe5 feat(auth):实现OAuth认证 2025-05-30 00:01:29 +08:00
li1553770945
c50c75821c feat: 迁移至ES模块(ESM)并配置TSX支持 2025-05-29 02:02:14 +08:00
li1553770945
df2716fa7a Merge branch 'feat/auth' of https://github.com/LSTM-Kirigaya/openmcp-client into feat/auth 2025-05-26 21:41:13 +08:00
li1553770945
b1f0675b5e fix:修复streamable_http连接显示失败的bug 2025-05-26 21:41:09 +08:00
Li Yaning
fc68a12b30
Merge pull request #20 from LSTM-Kirigaya/main
sync
2025-05-26 21:40:31 +08:00
29 changed files with 586 additions and 3437 deletions

3514
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -214,8 +214,7 @@
}, },
"workspaces": [ "workspaces": [
"service", "service",
"renderer", "renderer"
"software"
], ],
"scripts": { "scripts": {
"setup": "npm i && npm run prepare:ocr", "setup": "npm i && npm run prepare:ocr",
@ -237,6 +236,7 @@
"@seald-io/nedb": "^4.1.1", "@seald-io/nedb": "^4.1.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"bson": "^6.8.0", "bson": "^6.8.0",
"https-proxy-agent": "^7.0.6",
"openai": "^5.0.1", "openai": "^5.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",
"tesseract.js": "^6.0.1", "tesseract.js": "^6.0.1",

View File

@ -132,6 +132,7 @@ import ConnectInterfaceOpenai from './connect-interface-openai.vue';
import ConnectTest from './connect-test.vue'; import ConnectTest from './connect-test.vue';
import { llmSettingRef, makeSimpleTalk, simpleTestResult } from './api'; import { llmSettingRef, makeSimpleTalk, simpleTestResult } from './api';
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { mcpSetting } from '@/hook/mcp';
defineComponent({ name: 'api' }); defineComponent({ name: 'api' });
const { t } = useI18n(); const { t } = useI18n();
@ -233,11 +234,13 @@ async function updateModels() {
const llm = llms[llmManager.currentModelIndex]; const llm = llms[llmManager.currentModelIndex];
const apiKey = llm.userToken; const apiKey = llm.userToken;
const baseURL = llm.baseUrl; const baseURL = llm.baseUrl;
const proxyServer = mcpSetting.proxyServer;
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('llm/models', { const { code, msg } = await bridge.commandRequest('llm/models', {
apiKey, apiKey,
baseURL baseURL,
proxyServer
}); });
const isGemini = baseURL.includes('googleapis'); const isGemini = baseURL.includes('googleapis');

View File

@ -28,7 +28,7 @@ const { t } = useI18n();
const isGoogle = computed(() => { const isGoogle = computed(() => {
const model = llms[llmManager.currentModelIndex]; const model = llms[llmManager.currentModelIndex];
return model.userModel.startsWith('gemini') || model.baseUrl.includes('googleapis'); return model.userModel?.startsWith('gemini') || model.baseUrl.includes('googleapis');
}); });
console.log(llms[llmManager.currentModelIndex]); console.log(llms[llmManager.currentModelIndex]);

View File

@ -4,16 +4,15 @@
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"type": "commonjs", "type": "module",
"scripts": { "scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/main.ts", "serve": "nodemon --watch src --exec tsx src/main.ts",
"serve": "ts-node-dev --respawn --transpile-only src/main.ts",
"build": "tsc", "build": "tsc",
"build:watch": "tsc --watch", "build:watch": "tsc --watch",
"postbuild": "node ./scripts/post-build.mjs", "postbuild": "node ./scripts/post-build.mjs",
"start": "node dist/main.js", "start": "node --experimental-specifier-resolution=node dist/main.js",
"start:prod": "NODE_ENV=production node dist/main.js", "start:prod": "NODE_ENV=production node --experimental-specifier-resolution=node dist/main.js",
"debug": "node --inspect -r ts-node/register src/main.ts", "debug": "node --inspect --no-warnings=ExperimentalWarning -r tsx/esm src/main.ts",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"lint": "eslint src --ext .ts,.tsx", "lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@ -29,9 +28,10 @@
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/ws": "^8.18.0", "@types/ws": "^8.18.0",
"esbuild": "^0.25.3", "esbuild": "^0.25.3",
"ts-node": "^10.9.2", "nodemon": "^3.1.10",
"ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.6.3", "tsx": "^4.19.4",
"typescript": "^5.8.3",
"webpack-cli": "^6.0.1", "webpack-cli": "^6.0.1",
"webpack-node-externals": "^3.0.0" "webpack-node-externals": "^3.0.0"
}, },
@ -39,6 +39,8 @@
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@seald-io/nedb": "^4.1.1", "@seald-io/nedb": "^4.1.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"https-proxy-agent": "^7.0.6",
"open": "^10.1.2",
"openai": "^5.0.1", "openai": "^5.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",
"pino": "^9.6.0", "pino": "^9.6.0",

View File

@ -1,5 +1,5 @@
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter.js";
import { McpClient } from "../mcp/client.service"; import { McpClient } from "../mcp/client.service.js";
export type RequestClientType = McpClient | undefined; export type RequestClientType = McpClient | undefined;

View File

@ -1,5 +1,4 @@
import { MapperDescriptor, RequestHandler, RequestClientType, RestfulResponse, ControllerOption, RequestHandlerStore } from "./index.dto"; import { MapperDescriptor, RestfulResponse, ControllerOption, RequestHandlerStore } from "./index.dto.js";
export { MapperDescriptor, RequestHandler, RequestClientType };
export const requestHandlerStorage = new Map<string, RequestHandlerStore<any, RestfulResponse>>(); export const requestHandlerStorage = new Map<string, RequestHandlerStore<any, RestfulResponse>>();

View File

@ -1,11 +1,11 @@
import { requestHandlerStorage } from "."; import { requestHandlerStorage } from "./index.js";
import type { PostMessageble } from "../hook/adapter"; import type { PostMessageble } from "../hook/adapter.js";
import { LlmController } from "../llm/llm.controller"; import { LlmController } from "../llm/llm.controller.js";
import { ClientController } from "../mcp/client.controller"; import { ClientController } from "../mcp/client.controller.js";
import { ConnectController } from "../mcp/connect.controller"; import { ConnectController } from "../mcp/connect.controller.js";
import { OcrController } from "../mcp/ocr.controller"; import { OcrController } from "../mcp/ocr.controller.js";
import { PanelController } from "../panel/panel.controller"; import { PanelController } from "../panel/panel.controller.js";
import { SettingController } from "../setting/setting.controller"; import { SettingController } from "../setting/setting.controller.js";
export const ModuleControllers = [ export const ModuleControllers = [
ConnectController, ConnectController,

View File

@ -1,8 +1,8 @@
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { routeMessage } from '../common/router'; import { routeMessage } from '../common/router.js';
import { McpOptions } from '../mcp/client.dto'; import { McpOptions } from '../mcp/client.dto.js';
import { clientMap, connectService } from '../mcp/connect.service'; import { clientMap, connectService } from '../mcp/connect.service.js';
// WebSocket 消息格式 // WebSocket 消息格式
export interface WebSocketMessage { export interface WebSocketMessage {
@ -136,7 +136,7 @@ export class TaskLoopAdapter {
for (const client of clientMap.values()) { for (const client of clientMap.values()) {
const clientTools = await client?.listTools(); const clientTools = await client?.listTools();
if (clientTools?.tools) { if (clientTools?.tools) {
const enabledTools = clientTools.tools.map((tool) => { const enabledTools = clientTools.tools.map((tool: any) => {
const enabledTools = {...tool, enabled: true }; const enabledTools = {...tool, enabled: true };
return enabledTools; return enabledTools;
}); });

View File

@ -9,6 +9,7 @@ interface Entity {
} }
const dbConnections: Record<string, any> = {}; const dbConnections: Record<string, any> = {};
const DatastoreCtor = Datastore as unknown as { new(options: any): any };
export class LocalDB<T extends Entity> { export class LocalDB<T extends Entity> {
private db: any; private db: any;
@ -28,7 +29,7 @@ export class LocalDB<T extends Entity> {
const filename = path.join(dbPath, `${this.tableName}.db`); const filename = path.join(dbPath, `${this.tableName}.db`);
if (!dbConnections[filename]) { if (!dbConnections[filename]) {
dbConnections[filename] = new Datastore({ dbConnections[filename] = new DatastoreCtor({
filename, filename,
autoload: true, autoload: true,
timestampData: true timestampData: true

View File

@ -1,5 +1,5 @@
export { routeMessage } from './common/router'; export { routeMessage } from './common/router.js';
export { VSCodeWebViewLike, TaskLoopAdapter } from './hook/adapter'; export { VSCodeWebViewLike, TaskLoopAdapter } from './hook/adapter.js';
export { setVscodeWorkspace, setRunningCWD } from './hook/setting'; export { setVscodeWorkspace, setRunningCWD } from './hook/setting.js';
// TODO: 更加规范 // TODO: 更加规范
export { clientMap } from './mcp/connect.service'; export { clientMap } from './mcp/connect.service.js';

View File

@ -1,9 +1,11 @@
import { RequestClientType } from "../common/index.dto.js";
import { Controller } from "../common/index.js";
import { RequestData } from "../common/index.dto.js";
import { PostMessageble } from "../hook/adapter.js";
import { getClient } from "../mcp/connect.service.js";
import { abortMessageService, streamingChatCompletion } from "./llm.service.js";
import { OpenAI } from "openai"; import { OpenAI } from "openai";
import { Controller } from "../common"; import { axiosFetch } from "src/hook/axios-fetch.js";
import { RequestData } from "../common/index.dto";
import { PostMessageble } from "../hook/adapter";
import { abortMessageService, streamingChatCompletion } from "./llm.service";
export class LlmController { export class LlmController {
@Controller('llm/chat/completions') @Controller('llm/chat/completions')
@ -40,9 +42,14 @@ export class LlmController {
const { const {
baseURL, baseURL,
apiKey, apiKey,
} = data; proxyServer
} = data;
const client = new OpenAI({ apiKey, baseURL });
const client = new OpenAI({
apiKey,
baseURL,
});
const models = await client.models.list(); const models = await client.models.list();
return { return {

View File

@ -1,11 +1,11 @@
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter.js";
import { OpenAI } from "openai"; import { OpenAI } from "openai";
import { MyMessageType, MyToolMessageType } from "./llm.dto"; import { MyMessageType, MyToolMessageType } from "./llm.dto.js";
import { RestfulResponse } from "../common/index.dto"; import { RestfulResponse } from "../common/index.dto.js";
import { ocrDB } from "../hook/db"; import { ocrDB } from "../hook/db.js";
import type { ToolCallContent } from "../mcp/client.dto"; import type { ToolCallContent } from "../mcp/client.dto.js";
import { ocrWorkerStorage } from "../mcp/ocr.service"; import { ocrWorkerStorage } from "../mcp/ocr.service.js";
import { axiosFetch } from "../hook/axios-fetch"; import { axiosFetch } from "../hook/axios-fetch.js";
export let currentStream: AsyncIterable<any> | null = null; export let currentStream: AsyncIterable<any> | null = null;

View File

@ -1,11 +1,12 @@
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import pino from 'pino'; import {pino} from 'pino';
import { fileURLToPath } from 'url';
import { routeMessage } from './common/router'; import { dirname } from 'path';
import { VSCodeWebViewLike } from './hook/adapter'; import { routeMessage } from './common/router.js';
import { VSCodeWebViewLike } from './hook/adapter.js';
import path from 'node:path'; import path from 'node:path';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { setRunningCWD } from './hook/setting'; import { setRunningCWD } from './hook/setting.js';
import axios from 'axios'; import axios from 'axios';
export interface VSCodeMessage { export interface VSCodeMessage {
@ -85,7 +86,7 @@ function updateConnectionOption(data: any) {
fs.writeFileSync(envPath, JSON.stringify(connection, null, 4)); fs.writeFileSync(envPath, JSON.stringify(connection, null, 4));
} }
const __dirname = dirname(fileURLToPath(import.meta.url));
const devHome = path.join(__dirname, '..', '..'); const devHome = path.join(__dirname, '..', '..');
setRunningCWD(devHome); setRunningCWD(devHome);

View File

@ -0,0 +1,186 @@
import { createServer } from 'node:http';
import { URL } from 'node:url';
import { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import open from 'open';
// const CALLBACK_PORT = 16203; // Use different port than auth server (3001)
// const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
/**
* @description OAuth客户端提供者
*/
class InMemoryOAuthClientProvider implements OAuthClientProvider {
private _clientInformation?: OAuthClientInformationFull;
private _tokens?: OAuthTokens;
private _codeVerifier?: string;
constructor(
private readonly _redirectUrl: string | URL,
private readonly _clientMetadata: OAuthClientMetadata,
onRedirect?: (url: URL) => void
) {
this._onRedirect = onRedirect || ((url) => {
console.log(`Redirect to: ${url.toString()}`);
});
}
private _onRedirect: (url: URL) => void;
get redirectUrl(): string | URL {
return this._redirectUrl;
}
get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}
clientInformation(): OAuthClientInformation | undefined {
return this._clientInformation;
}
saveClientInformation(clientInformation: OAuthClientInformationFull): void {
this._clientInformation = clientInformation;
}
tokens(): OAuthTokens | undefined {
return this._tokens;
}
saveTokens(tokens: OAuthTokens): void {
this._tokens = tokens;
}
redirectToAuthorization(authorizationUrl: URL): void {
this._onRedirect(authorizationUrl);
}
saveCodeVerifier(codeVerifier: string): void {
this._codeVerifier = codeVerifier;
}
codeVerifier(): string {
if (!this._codeVerifier) {
throw new Error('No code verifier saved');
}
return this._codeVerifier;
}
}
export class OAuthClient {
port: number;
callbackUrl: string;
constructor() {
console.log('🔐 Initializing OAuth client...');
// 初始化OAuth客户端
this.port = Math.floor(Math.random() * (50000 - 40000 + 1)) + 40000;
//TODO: 如果端口被占用,重新生成一个端口
this.callbackUrl = `http://localhost:${this.port}/callback`;
}
/**
* @description OAuth回调请求
* @returns {Promise<string>}
* @throws {Error}
*/
public async waitForOAuthCallback(): Promise<string> {
return new Promise<string>((resolve, reject) => {
const server = createServer((req, res) => {
// Ignore favicon requests
if (req.url === '/favicon.ico') {
res.writeHead(404);
res.end();
return;
}
console.log(`📥 Received callback: ${req.url}`);
const parsedUrl = new URL(req.url || '', 'http://localhost');
const code = parsedUrl.searchParams.get('code');
const error = parsedUrl.searchParams.get('error');
if (code) {
console.log(`✅ Authorization code received: ${code?.substring(0, 10)}...`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Authorization Successful!</h1>
<p>You can close this window and return to the terminal.</p>
<script>setTimeout(() => window.close(), 2000);</script>
</body>
</html>
`);
resolve(code);
setTimeout(() => server.close(), 3000);
} else if (error) {
console.log(`❌ Authorization error: ${error}`);
res.writeHead(400, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Authorization Failed</h1>
<p>Error: ${error}</p>
</body>
</html>
`);
reject(new Error(`OAuth authorization failed: ${error}`));
} else {
console.log(`❌ No authorization code or error in callback`);
res.writeHead(400);
res.end('Bad request');
reject(new Error('No authorization code provided'));
}
});
server.listen(this.port, () => {
console.log(`OAuth callback server started on http://localhost:${this.port}`);
});
});
}
/**
* @description Oauth认证provider
* @return {Promise<OAuthClientProvider>} OAuthClientProvider实例
*/
public async getOAuthProvider(): Promise<OAuthClientProvider> {
const clientMetadata: OAuthClientMetadata = {
client_name: 'Simple OAuth MCP Client',
redirect_uris: [this.callbackUrl],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
};
console.log('🔐 Creating OAuth provider...');
const oauthProvider = new InMemoryOAuthClientProvider(
this.callbackUrl,
clientMetadata,
(redirectUrl: URL) => {
console.log(`📌 OAuth redirect handler called - opening browser`);
console.log(`Opening browser to: ${redirectUrl.toString()}`);
this.openBrowser(redirectUrl.toString());
}
);
console.log('🔐 OAuth provider created');
return oauthProvider;
}
/**
* @description
* @param url URL
*/
public async openBrowser(url: string): Promise<void> {
console.log(`🌐 Opening browser for authorization: ${url}`);
await open(url); // 自动适配不同操作系统
}
}

View File

@ -1,8 +1,8 @@
import { Controller } from "../common"; import { Controller } from "../common/index.js";
import { RequestData } from "../common/index.dto"; import { RequestData } from "../common/index.dto.js";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter.js";
import { postProcessMcpToolcallResponse } from "./client.service"; import { postProcessMcpToolcallResponse } from "./client.service.js";
import { getClient } from "./connect.service"; import { getClient } from "./connect.service.js";
export class ClientController { export class ClientController {

View File

@ -1,7 +1,7 @@
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Implementation } from "@modelcontextprotocol/sdk/types"; import { Implementation } from "@modelcontextprotocol/sdk/types.js";
export interface GetPromptOption { export interface GetPromptOption {
promptId: string; promptId: string;

View File

@ -3,16 +3,20 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { McpOptions, McpTransport, IServerVersion, ToolCallResponse, ToolCallContent } from './client.dto'; import type { McpOptions, McpTransport, IServerVersion, ToolCallResponse, ToolCallContent } from './client.dto.js';
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter.js";
import { createOcrWorker, saveBase64ImageData } from "./ocr.service"; import { createOcrWorker, saveBase64ImageData } from "./ocr.service.js";
import { OAuthClient } from "./auth.service.js";
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
// 增强的客户端类 // 增强的客户端类
export class McpClient { export class McpClient {
private client: Client; private client: Client;
private transport?: McpTransport; private transport?: McpTransport;
private options: McpOptions; private options: McpOptions;
private serverVersion: IServerVersion; private serverVersion: IServerVersion;
private oAuthClient: OAuthClient;
private oauthPovider?: OAuthClientProvider;
constructor(options: McpOptions) { constructor(options: McpOptions) {
this.options = options; this.options = options;
@ -31,14 +35,18 @@ export class McpClient {
} }
} }
); );
this.oAuthClient = new OAuthClient();
} }
// 连接方法 // 连接方法
public async connect(): Promise<void> { public async connect(): Promise<void> {
if (!this.oauthPovider){
this.oauthPovider = await this.oAuthClient.getOAuthProvider();
}
// 根据连接类型创建传输层 // 根据连接类型创建传输层
switch (this.options.connectionType) { switch (this.options.connectionType) {
case 'STDIO': case 'STDIO':
this.transport = new StdioClientTransport({ this.transport = new StdioClientTransport({
command: this.options.command || '', command: this.options.command || '',
args: this.options.args || [], args: this.options.args || [],
@ -55,20 +63,22 @@ export class McpClient {
this.transport = new SSEClientTransport( this.transport = new SSEClientTransport(
new URL(this.options.url), new URL(this.options.url),
{ {
// authProvider: authProvider: this.oauthPovider
} }
); );
break; break;
case 'STREAMABLE_HTTP': case 'STREAMABLE_HTTP':
if (!this.options.url) { if (!this.options.url) {
throw new Error('URL is required for STREAMABLE_HTTP connection'); throw new Error('URL is required for STREAMABLE_HTTP connection');
} }
this.transport = new StreamableHTTPClientTransport( this.transport = new StreamableHTTPClientTransport(
new URL(this.options.url) new URL(this.options.url),
{
authProvider:this.oauthPovider
}
); );
break; break;
default: default:
throw new Error(`Unsupported connection type: ${this.options.connectionType}`); throw new Error(`Unsupported connection type: ${this.options.connectionType}`);
@ -76,11 +86,32 @@ export class McpClient {
// 建立连接 // 建立连接
if (this.transport) { if (this.transport) {
await this.client.connect(this.transport); try {
console.log(`Connected to MCP server via ${this.options.connectionType}`); console.log(`🔌 Connecting to MCP server via ${this.options.connectionType}...`);
await this.client.connect(this.transport);
console.log(`Connected to MCP server via ${this.options.connectionType}`);
} catch (error) {
if (error instanceof UnauthorizedError) {
if (!(this.transport instanceof StreamableHTTPClientTransport) && !(this.transport instanceof SSEClientTransport)) {
console.error('❌ OAuth is only supported for StreamableHTTP and SSE transports. Please use one of these transports for OAuth authentication.');
return;
}
console.log('🔐 OAuth required - waiting for authorization...');
const callbackPromise = this.oAuthClient.waitForOAuthCallback();
const authCode = await callbackPromise;
await this.transport.finishAuth(authCode);
console.log('🔐 Authorization code received:', authCode);
console.log('🔌 Reconnecting with authenticated transport...');
await this.connect(); // 递归重试
} else {
console.error('❌ Connection failed with non-auth error:', error);
throw error;
}
}
} }
} }
public getServerVersion() { public getServerVersion() {
if (this.serverVersion) { if (this.serverVersion) {
return this.serverVersion; return this.serverVersion;
@ -94,7 +125,7 @@ export class McpClient {
// 断开连接 // 断开连接
public async disconnect(): Promise<void> { public async disconnect(): Promise<void> {
await this.client.close(); await this.client.close();
console.log('Disconnected from MCP server'); console.log('Disconnected from MCP server');
} }
@ -104,7 +135,7 @@ export class McpClient {
} }
// 获取提示 // 获取提示
public async getPrompt(name: string, args: Record<string, any> = {}) { public async getPrompt(name: string, args: Record<string, any> = {}) {
return await this.client.getPrompt({ return await this.client.getPrompt({
name, arguments: args name, arguments: args
}); });
@ -136,7 +167,6 @@ export class McpClient {
public async callTool(options: { name: string; arguments: Record<string, any>, callToolOption?: any }) { public async callTool(options: { name: string; arguments: Record<string, any>, callToolOption?: any }) {
const { callToolOption, ...methodArgs } = options; const { callToolOption, ...methodArgs } = options;
const res = await this.client.callTool(methodArgs, undefined, callToolOption); const res = await this.client.callTool(methodArgs, undefined, callToolOption);
return res; return res;
} }
} }
@ -155,10 +185,10 @@ async function handleImage(
if (content.data && content.mimeType) { if (content.data && content.mimeType) {
const filename = saveBase64ImageData(content.data, content.mimeType); const filename = saveBase64ImageData(content.data, content.mimeType);
content.data = filename; content.data = filename;
// 加入工作线程 // 加入工作线程
const worker = createOcrWorker(filename, webview); const worker = createOcrWorker(filename, webview);
content._meta = { content._meta = {
ocr: true, ocr: true,
workerId: worker.id workerId: worker.id
@ -189,7 +219,7 @@ export function postProcessMcpToolcallResponse(
case 'image': case 'image':
handleImage(content, webview); handleImage(content, webview);
break; break;
default: default:
break; break;
} }

View File

@ -1,7 +1,7 @@
import { Controller } from '../common'; import { Controller } from '../common/index.js';
import { PostMessageble } from '../hook/adapter'; import { PostMessageble } from '../hook/adapter.js';
import { RequestData } from '../common/index.dto'; import { RequestData } from '../common/index.dto.js';
import { connectService, getClient } from './connect.service'; import { connectService, getClient } from './connect.service.js';
export class ConnectController { export class ConnectController {

View File

@ -1,13 +1,13 @@
import { exec, execSync, spawnSync } from 'node:child_process'; import { exec, execSync, spawnSync } from 'node:child_process';
import { RequestClientType } from '../common'; import { RequestClientType } from '../common/index.dto.js';
import { connect } from './client.service'; import { connect } from './client.service.js';
import { RestfulResponse } from '../common/index.dto'; import { RestfulResponse } from '../common/index.dto.js';
import { McpOptions } from './client.dto'; import { McpOptions } from './client.dto.js';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import * as os from 'os'; import * as os from 'os';
import { PostMessageble } from '../hook/adapter'; import { PostMessageble } from '../hook/adapter.js';
export const clientMap: Map<string, RequestClientType> = new Map(); export const clientMap: Map<string, RequestClientType> = new Map();
export function getClient(clientId?: string): RequestClientType | undefined { export function getClient(clientId?: string): RequestClientType | undefined {

View File

@ -1,7 +1,8 @@
import { Controller, RequestClientType } from "../common"; import { RequestClientType } from "../common/index.dto.js";
import { PostMessageble } from "../hook/adapter"; import { Controller } from "../common/index.js";
import { diskStorage } from "../hook/db"; import { PostMessageble } from "../hook/adapter.js";
import { createOcrWorker, saveBase64ImageData } from "./ocr.service"; import { diskStorage } from "../hook/db.js";
import { createOcrWorker, saveBase64ImageData } from "./ocr.service.js";
export class OcrController { export class OcrController {
@Controller('ocr/get-ocr-image') @Controller('ocr/get-ocr-image')

View File

@ -1,12 +1,12 @@
import Tesseract from 'tesseract.js'; import Tesseract from 'tesseract.js';
import { PostMessageble } from '../hook/adapter'; import { PostMessageble } from '../hook/adapter.js';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { OcrWorker } from './ocr.dto'; import { OcrWorker } from './ocr.dto.js';
import { diskStorage, ocrDB } from '../hook/db'; import { diskStorage, ocrDB } from '../hook/db.js';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { RUNNING_CWD } from '../hook/setting'; import { RUNNING_CWD } from '../hook/setting.js';
export const ocrWorkerStorage = new Map<string, OcrWorker>(); export const ocrWorkerStorage = new Map<string, OcrWorker>();

View File

@ -1,9 +1,9 @@
import { Controller } from "../common"; import { Controller } from "../common/index.js";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter.js";
import { RequestData } from "../common/index.dto"; import { RequestData } from "../common/index.dto.js";
import { getClient } from "../mcp/connect.service"; import { getClient } from "../mcp/connect.service.js";
import { systemPromptDB } from "../hook/db"; import { systemPromptDB } from "../hook/db.js";
import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service"; import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service.js";
export class PanelController { export class PanelController {
@Controller('panel/save') @Controller('panel/save')

View File

@ -1,9 +1,9 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { VSCODE_WORKSPACE } from '../hook/setting'; import { VSCODE_WORKSPACE } from '../hook/setting.js';
import { IServerVersion } from '../mcp/client.dto'; import { IServerVersion } from '../mcp/client.dto.js';
import { SaveTab } from './panel.dto'; import { SaveTab } from './panel.dto.js';
import { IConfig } from '../setting/setting.dto'; import { IConfig } from '../setting/setting.dto.js';
const DEFAULT_TABS: SaveTab = { const DEFAULT_TABS: SaveTab = {
tabs: [], tabs: [],

View File

@ -1,11 +1,11 @@
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import pino from 'pino'; import pino from 'pino';
import { routeMessage } from './common/router'; import { routeMessage } from './common/router.js';
import { VSCodeWebViewLike } from './hook/adapter'; import { VSCodeWebViewLike } from './hook/adapter.js';
import path from 'node:path'; import path from 'node:path';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { setRunningCWD } from './hook/setting'; import { setRunningCWD } from './hook/setting.js';
import { exit } from 'node:process'; import { exit } from 'node:process';
export interface VSCodeMessage { export interface VSCodeMessage {
@ -14,7 +14,7 @@ export interface VSCodeMessage {
callbackId?: string; callbackId?: string;
} }
const logger = pino({ const logger = pino.default({
transport: { transport: {
target: 'pino-pretty', // 启用 pino-pretty target: 'pino-pretty', // 启用 pino-pretty
options: { options: {

View File

@ -1,8 +1,8 @@
import { Controller } from "../common"; import { Controller } from "../common/index.js";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter.js";
import { RequestData } from "../common/index.dto"; import { RequestData } from "../common/index.dto.js";
import { getClient } from "../mcp/connect.service"; import { getClient } from "../mcp/connect.service.js";
import { getTour, loadSetting, saveSetting, setTour } from "./setting.service"; import { getTour, loadSetting, saveSetting, setTour } from "./setting.service.js";
export class SettingController { export class SettingController {

View File

@ -1,9 +1,9 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { VSCODE_WORKSPACE } from '../hook/setting'; import { VSCODE_WORKSPACE } from '../hook/setting.js';
import { IConfig } from './setting.dto'; import { IConfig } from './setting.dto.js';
import { llms } from '../hook/llm'; import { llms } from '../hook/llm.js';
function getConfigurationPath() { function getConfigurationPath() {
const homeDir = os.homedir(); const homeDir = os.homedir();

View File

@ -1,7 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"target": "ES6", "target": "ES6",
"module": "commonjs", "module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -10,13 +12,16 @@
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node"
}, },
"paths": { "paths": {
"@/*": [ "@/*": [
"src/*" "src/*"
] ]
}, },
"include": ["src/**/*"], "include": [
"exclude": ["node_modules", "src/main.ts"] // main.ts "src/**/*"
} ],
"exclude": [
"node_modules",
]
}

View File

@ -16,25 +16,37 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
getTreeItem(element: ConnectionViewItem): vscode.TreeItem { getTreeItem(element: ConnectionViewItem): vscode.TreeItem {
return element; return element;
} }
getChildren(element?: ConnectionViewItem): Thenable<ConnectionViewItem[]> { getChildren(element?: ConnectionViewItem): Thenable<ConnectionViewItem[]> {
// TODO: 读取 configDir 下的所有文件,作为子节点 // TODO: 读取 configDir 下的所有文件,作为子节点
const connection = getWorkspaceConnectionConfig(); const connection = getWorkspaceConnectionConfig();
const sidebarItems = connection.items.map((item, index) => {
// 连接的名字 // 校验 connection 和 connection.items
const nItem = Array.isArray(item) ? item[0] : item; if (!connection || !Array.isArray(connection.items)) {
const itemName = `${nItem.name} (${nItem.type || nItem.connectionType})` return Promise.resolve([]);
return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server'); }
})
const sidebarItems = connection.items
.filter(item => item !== null && item !== undefined)
.map((item, index) => {
// 连接的名字
const nItem = Array.isArray(item) ? item[0] : item;
if (!nItem || typeof nItem !== 'object') {
return null;
}
const name = nItem.name || '未命名';
const type = nItem.type || nItem.connectionType || '未知类型';
const itemName = `${name} (${type})`;
return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server');
})
.filter(Boolean) as ConnectionViewItem[]; // 过滤掉为 null 的项
// 返回子节点 // 返回子节点
return Promise.resolve(sidebarItems); return Promise.resolve(sidebarItems);
} }
@RegisterCommand('revealWebviewPanel') @RegisterCommand('revealWebviewPanel')
public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) { public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) {
const item = view.item; const item = view.item;
const masterNode = Array.isArray(item)? item[0] : item; const masterNode = Array.isArray(item) ? item[0] : item;
const name = masterNode.filePath || masterNode.name || ''; const name = masterNode.filePath || masterNode.name || '';
revealOpenMcpWebviewPanel(context, 'workspace', name, item); revealOpenMcpWebviewPanel(context, 'workspace', name, item);
} }