MCP Client Auth 说明
流程总览
MCP SDK Client的Python版本暂未提供Auth流程,使用Typescript SDK的v1.11.1进行认证流程演示
- client的sdk在收到服务端401响应时,从
openapi.wps.cn
获取/.well-known的认证服务信息 - client再与
openapi.wps.cn
的认证服务进行token的换取 - 最终sdk会在请求中携带Authorization: bearer token保证可以正常使用
openapi.wps.cn
的mcp功能
前置条件
- 准备open.wps.cn应用,并得到
APPID
与APPKEY
,参数说明见:https://open.wps.cn/documents/app-integration-dev/server/api-description/noun-description.html - 本地准备好一个认证服务的回调地址,并添加到用户授权回调地址中:https://open.wps.cn/documents/app-integration-dev/guide/self-app/develop-app.html#配置安全设置
- 关闭接口签名中的接口签名开关
- 鉴权接口会为所有MCP Server能力申请权限,请确保 [工具列表] 中的必要权限均已开启
关键代码
1. 实现OAuthClientProvider
TypeScript
class MyAuthProvider implements OAuthClientProvider {
private tokens_: OAuthTokens | undefined;
private codeVerifier_: string | undefined;
get redirectUrl(): string {
return authRedirectUri;
}
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [],
client_name: ""
};
}
clientInformation(): OAuthClientInformation {
return {
client_id: appId, // 配置为应用的APPID
client_secret: appKey, // 配置为应用的APPKEY
};
}
async tokens(): Promise<OAuthTokens | undefined> {
return this.tokens_
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
this.tokens_ = tokens;
if (!this.tokens_) {
console.log("Received new tokens:", tokens)
}
}
async redirectToAuthorization(authUrl: URL): Promise<void> {
// 这里根据环境自行处理认证及重定向,如:
// 浏览器端可直接window.location.href = authUrl.href
// 服务端可通知客户端代码进行处理
console.log("Redirecting to:", authUrl.href);
}
async saveCodeVerifier(verifier: string): Promise<void> {
this.codeVerifier_ = verifier;
}
async codeVerifier(): Promise<string> {
if (!this.codeVerifier_) {
throw new Error("No code verifier saved");
}
return this.codeVerifier_;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2. Transport配置authProvider
TypeScript
const authProvider = new MyAuthProvider()
const transport = new SSEClientTransport(new URL(serverUrl), { authProvider })
1
2
2
3. 配置认证回调接口并换取token
TypeScript
app.get(`/callback`, async (req, res) => {
console.log("Got callback");
const code = req.query.code;
await transport.finishAuth(code as string);
})
1
2
3
4
5
2
3
4
5
完成上述步骤后,token会存储在authProvider中,后续网络请求时,会自动带入header
4. 重连client并执行后续操作
TypeScript
await client.connect(transport);
await client.request({
method: 'tools/list',
params: {}
}, ListToolsResultSchema)
1
2
3
4
5
2
3
4
5
完整示例
TypeScript
import express from "express";
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
import type {
OAuthTokens,
OAuthClientMetadata,
OAuthClientInformation,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import {
ListToolsRequest,
ListToolsResultSchema,
CallToolResult,
CallToolResultSchema,
LoggingMessageNotificationSchema,
} from '@modelcontextprotocol/sdk/types.js';
// 需配置WPS开放平台应用的APPID和APPKEY,以及在app应用中配置授权回调地址
const appId = process.env.APPID || "my-app-id";
const appKey = process.env.APPKEY || "my-app-key";
const authRedirectUri = process.env.AUTH_CALLBACK || "http://localhost:8080/callback";
const args = process.argv.slice(2);
const port = parseInt(args[0] || '8080');
const serverUrl = args[1] || 'http://localhost:3000/mcp/sse';
function createTransport(): SSEClientTransport {
return new SSEClientTransport(new URL(serverUrl), { authProvider });
}
class MyAuthProvider implements OAuthClientProvider {
private tokens_: OAuthTokens | undefined;
private codeVerifier_: string | undefined;
get redirectUrl(): string {
return authRedirectUri;
}
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [],
client_name: ""
};
}
clientInformation(): OAuthClientInformation {
return {
client_id: appId,
client_secret: appKey,
};
}
async tokens(): Promise<OAuthTokens | undefined> {
return this.tokens_
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
this.tokens_ = tokens;
if (!this.tokens_) {
console.log("Received new tokens:", tokens)
}
}
async redirectToAuthorization(authUrl: URL): Promise<void> {
console.log("Redirecting to:", authUrl.href);
// In a browser, you might do: window.location.href = authUrl.href;
}
async saveCodeVerifier(verifier: string): Promise<void> {
this.codeVerifier_ = verifier;
}
async codeVerifier(): Promise<string> {
if (!this.codeVerifier_) {
throw new Error("No code verifier saved");
}
return this.codeVerifier_;
}
}
const authProvider = new MyAuthProvider()
async function runServer(port: number | null, client: Client): Promise<void> {
if (port !== null) {
const app = express();
app.use(express.json());
app.get(`/callback`, async (req, res) => {
console.log("Got callback");
const code = req.query.code;
if (code) {
console.log("Received code:", code);
const transport = createTransport();
await transport.finishAuth(code as string);
res.send("Authorization successful! You can close this window.");
await client.connect(transport);
console.log("Auto connected using Sse transport");
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
console.log(`Notification: ${notification.params.data}`);
});
} else {
res.status(400).send("No authorization code received.");
}
})
app.post('/toolslist', async (req, res) => {
console.log("Received tools list request");
const tools = await listTools(client);
res.json(tools);
});
app.post('/toolscall', async (req, res) => {
console.log("Received tools call request");
const { name, args } = req.body;
const tools = await callTool(client, name, args);
res.json(tools);
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
} else {
console.error("Invalid port number");
return;
}
}
async function listTools(client: Client): Promise<Array<any>> {
try {
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
console.log(' No tools available');
return []
} else {
for (const tool of toolsResult.tools) {
console.log(` - ${tool.name}: ${tool.description}`);
}
return toolsResult.tools
}
} catch (error) {
console.log(`Tools not supported by this server: ${error}`);
throw error
}
}
async function callTool(client: Client, name: string, args: {[key: string]: string | number | boolean}): Promise<CallToolResult> {
try {
return client.request({
method: 'tools/call',
params: {
name,
arguments: args,
}
}, CallToolResultSchema)
.catch(error => {
console.error(`Error in tool call:`, error);
throw error;
});
} catch (error) {
console.error(`Error starting parallel notification tools:`, error);
throw error;
}
}
async function main(): Promise<void> {
console.log('MCP Auth Sse Client');
console.log('===============================');
console.log(`Connecting to server at: ${serverUrl}`);
const client = new Client({
name: 'sse-client',
version: '1.0.0'
});
const transport = createTransport();
try {
await client.connect(transport);
console.log("Connected using Sse transport");
} catch (error) {
if (error instanceof UnauthorizedError) {
await client.close();
console.log('Need to authorize');
} else {
console.error('Error creating transport:', error);
process.exit(1);
}
}
await runServer(port, client);
}
main().catch((error: unknown) => {
console.error('Error running MCP client:', error);
process.exit(1);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203