@minko-ai/minko 0.1.0 → 0.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/README.md +577 -2
- package/assets/mascot-preview.png +0 -0
- package/assets/mascot-preview.txt +46 -0
- package/assets/mascot-source.png +0 -0
- package/assets/skills/README.md +21 -0
- package/assets/skills/brainstorming/LICENSE +21 -0
- package/assets/skills/brainstorming/SKILL.md +316 -0
- package/assets/skills/buddhist-method/LICENSE +21 -0
- package/assets/skills/buddhist-method/SKILL.md +137 -0
- package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
- package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
- package/assets/skills/code-humanizer/LICENSE +21 -0
- package/assets/skills/code-humanizer/SKILL.md +174 -0
- package/assets/skills/code-review/LICENSE +21 -0
- package/assets/skills/code-review/SKILL.md +232 -0
- package/assets/skills/code-review/reference/angular.md +768 -0
- package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/assets/skills/code-review/reference/c.md +890 -0
- package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
- package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/assets/skills/code-review/reference/cpp.md +893 -0
- package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/assets/skills/code-review/reference/csharp.md +525 -0
- package/assets/skills/code-review/reference/css-less-sass.md +661 -0
- package/assets/skills/code-review/reference/dart.md +670 -0
- package/assets/skills/code-review/reference/django.md +985 -0
- package/assets/skills/code-review/reference/fastapi.md +580 -0
- package/assets/skills/code-review/reference/go.md +993 -0
- package/assets/skills/code-review/reference/java.md +409 -0
- package/assets/skills/code-review/reference/java8.md +586 -0
- package/assets/skills/code-review/reference/kotlin.md +1018 -0
- package/assets/skills/code-review/reference/nestjs.md +593 -0
- package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
- package/assets/skills/code-review/reference/php.md +684 -0
- package/assets/skills/code-review/reference/python.md +1073 -0
- package/assets/skills/code-review/reference/qt.md +757 -0
- package/assets/skills/code-review/reference/react.md +871 -0
- package/assets/skills/code-review/reference/ruby.md +964 -0
- package/assets/skills/code-review/reference/rust.md +846 -0
- package/assets/skills/code-review/reference/security-review-guide.md +494 -0
- package/assets/skills/code-review/reference/svelte.md +1064 -0
- package/assets/skills/code-review/reference/swift.md +936 -0
- package/assets/skills/code-review/reference/typescript.md +1016 -0
- package/assets/skills/code-review/reference/vue.md +924 -0
- package/assets/skills/code-review/reference/zig.md +440 -0
- package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
- package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
- package/assets/skills/five-whys/LICENSE +21 -0
- package/assets/skills/five-whys/SKILL.md +247 -0
- package/assets/skills/five-whys/examples/performance-regression.md +161 -0
- package/assets/skills/five-whys/examples/production-incident.md +134 -0
- package/assets/skills/five-whys/references/software-patterns.md +284 -0
- package/assets/skills/five-whys/references/toyota-origins.md +257 -0
- package/assets/skills/systematic-debugging/LICENSE +21 -0
- package/assets/skills/systematic-debugging/SKILL.md +283 -0
- package/assets/skills/test-driven-development/LICENSE +21 -0
- package/assets/skills/test-driven-development/SKILL.md +330 -0
- package/assets/skills/verification-before-completion/LICENSE +21 -0
- package/assets/skills/verification-before-completion/SKILL.md +120 -0
- package/assets/skills/vibe-sec/LICENSE +201 -0
- package/assets/skills/vibe-sec/SKILL.md +758 -0
- package/assets/skills/writing-plans/LICENSE +21 -0
- package/assets/skills/writing-plans/SKILL.md +204 -0
- package/bin/minko.js +2 -2
- package/dist/agent/approvalTokens.js +224 -0
- package/dist/agent/branchLock.js +315 -0
- package/dist/agent/compact.js +182 -0
- package/dist/agent/context.js +183 -0
- package/dist/agent/cron.js +326 -0
- package/dist/agent/hooks.js +211 -0
- package/dist/agent/lanes.js +511 -0
- package/dist/agent/loop.js +337 -0
- package/dist/agent/memory.js +159 -0
- package/dist/agent/permissions.js +71 -0
- package/dist/agent/plan.js +118 -0
- package/dist/agent/prompt.js +120 -0
- package/dist/agent/subagent.js +67 -0
- package/dist/agent/tasks.js +449 -0
- package/dist/agent/transcript.js +62 -0
- package/dist/agent/workerStore.js +172 -0
- package/dist/agent/workerTypes.js +1 -0
- package/dist/agent/workers.js +503 -0
- package/dist/cli/app.js +261 -0
- package/dist/cli/argv.js +206 -0
- package/dist/cli/commandsCommand.js +12 -0
- package/dist/cli/configCommand.js +170 -0
- package/dist/cli/doctorCommand.js +112 -0
- package/dist/cli/exportCommand.js +85 -0
- package/dist/cli/forkCommand.js +24 -0
- package/dist/cli/linkCommand.js +272 -0
- package/dist/cli/loginCommand.js +69 -0
- package/dist/cli/mcpLoginCommand.js +68 -0
- package/dist/cli/pluginCommand.js +113 -0
- package/dist/cli/powerCommand.js +174 -0
- package/dist/cli/promptFooter.js +74 -0
- package/dist/cli/relayCommand.js +38 -0
- package/dist/cli/sandboxCommand.js +31 -0
- package/dist/cli/serveCommand.js +104 -0
- package/dist/cli/statusCommand.js +120 -0
- package/dist/cli/tui.js +1104 -0
- package/dist/commands/registry.js +99 -0
- package/dist/config/paths.js +81 -0
- package/dist/config/store.js +605 -0
- package/dist/index.js +199 -0
- package/dist/link/protocol.js +159 -0
- package/dist/link/relay.js +184 -0
- package/dist/link/server.js +566 -0
- package/dist/link/tunnel.js +90 -0
- package/dist/lsp/client.js +340 -0
- package/dist/lsp/tool.js +177 -0
- package/dist/mcp/client.js +469 -0
- package/dist/mcp/httpClient.js +191 -0
- package/dist/mcp/server.js +206 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/oauth/device.js +111 -0
- package/dist/oauth/store.js +56 -0
- package/dist/plugins/loader.js +291 -0
- package/dist/plugins/market.js +98 -0
- package/dist/providers/anthropic.js +279 -0
- package/dist/providers/factory.js +109 -0
- package/dist/providers/openai.js +182 -0
- package/dist/providers/retry.js +95 -0
- package/dist/providers/sse.js +67 -0
- package/dist/providers/types.js +10 -0
- package/dist/rag/embeddings.js +171 -0
- package/dist/rag/index.js +373 -0
- package/dist/rag/server.js +167 -0
- package/dist/rag/tool.js +148 -0
- package/dist/sandbox/index.js +55 -0
- package/dist/sdk/server.js +150 -0
- package/dist/sdk/session.js +104 -0
- package/dist/session/liveness.js +148 -0
- package/dist/session/recorder.js +49 -0
- package/dist/session/store.js +171 -0
- package/dist/skills/index.js +428 -0
- package/dist/theme.js +30 -0
- package/dist/tools/fs.js +195 -0
- package/dist/tools/notebook.js +109 -0
- package/dist/tools/registry.js +32 -0
- package/dist/tools/search.js +130 -0
- package/dist/tools/shell.js +200 -0
- package/dist/tools/structured.js +83 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/ansi.js +135 -0
- package/dist/ui/banner.js +103 -0
- package/dist/ui/input.js +49 -0
- package/dist/ui/inputModel.js +113 -0
- package/dist/ui/keyboard.js +142 -0
- package/dist/ui/mascot.js +34 -0
- package/dist/ui/mascotData.js +96 -0
- package/dist/ui/md.js +183 -0
- package/dist/ui/screen.js +49 -0
- package/dist/ui/spinner.js +52 -0
- package/dist/ui/transcript.js +287 -0
- package/dist/util/clipboard.js +53 -0
- package/dist/util/errors.js +50 -0
- package/dist/util/version.js +12 -0
- package/dist/web/fetch.js +168 -0
- package/dist/web/search.js +97 -0
- package/package.json +45 -36
- package/src/agent/loop.ts +0 -159
- package/src/agent/prompt.ts +0 -45
- package/src/cli/app.ts +0 -396
- package/src/cli/argv.ts +0 -139
- package/src/cli/configCommand.ts +0 -175
- package/src/config/paths.ts +0 -20
- package/src/config/store.ts +0 -149
- package/src/index.ts +0 -120
- package/src/providers/anthropic.ts +0 -215
- package/src/providers/openai.ts +0 -158
- package/src/providers/sse.ts +0 -57
- package/src/providers/types.ts +0 -59
- package/src/session/store.ts +0 -125
- package/src/theme.ts +0 -41
- package/src/tools/fs.ts +0 -143
- package/src/tools/registry.ts +0 -19
- package/src/tools/shell.ts +0 -120
- package/src/tools/types.ts +0 -26
- package/src/ui/ansi.ts +0 -110
- package/src/ui/banner.ts +0 -39
- package/src/ui/input.ts +0 -54
- package/src/ui/md.ts +0 -160
- package/src/ui/spinner.ts +0 -58
- package/src/util/errors.ts +0 -48
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { ALL_TOOLS } from "../tools/registry.js";
|
|
6
|
+
import { listSkills, readSkill } from "../skills/index.js";
|
|
7
|
+
import { readVersion } from "../util/version.js";
|
|
8
|
+
/** Minko as an MCP stdio server; read-only tools are exposed by default. */
|
|
9
|
+
export class McpServer {
|
|
10
|
+
cwd;
|
|
11
|
+
config;
|
|
12
|
+
tools;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
15
|
+
this.config = options.config;
|
|
16
|
+
this.tools = options.tools ?? ALL_TOOLS.filter((tool) => tool.readOnly);
|
|
17
|
+
}
|
|
18
|
+
async handle(message) {
|
|
19
|
+
const method = typeof message.method === 'string' ? message.method : '';
|
|
20
|
+
const id = message.id;
|
|
21
|
+
if (method === 'initialize') {
|
|
22
|
+
return result(id, {
|
|
23
|
+
protocolVersion: '2024-11-05',
|
|
24
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
25
|
+
serverInfo: { name: 'minko', version: readVersion() },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (method === 'notifications/initialized') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (method === 'tools/list') {
|
|
32
|
+
return result(id, {
|
|
33
|
+
tools: this.tools.map((tool) => ({
|
|
34
|
+
name: tool.spec.name,
|
|
35
|
+
description: tool.spec.description,
|
|
36
|
+
inputSchema: tool.spec.parameters,
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (method === 'tools/call') {
|
|
41
|
+
const params = message.params !== null && typeof message.params === 'object'
|
|
42
|
+
? message.params : {};
|
|
43
|
+
const name = typeof params.name === 'string' ? params.name : '';
|
|
44
|
+
const args = params.arguments !== null && typeof params.arguments === 'object'
|
|
45
|
+
? params.arguments : {};
|
|
46
|
+
const tool = this.tools.find((candidate) => candidate.spec.name === name);
|
|
47
|
+
if (!tool) {
|
|
48
|
+
return error(id, -32602, 'unknown tool: ' + name);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const text = await tool.run(args, {
|
|
52
|
+
cwd: this.cwd,
|
|
53
|
+
signal: new AbortController().signal,
|
|
54
|
+
approve: async () => false,
|
|
55
|
+
config: this.config,
|
|
56
|
+
});
|
|
57
|
+
return result(id, { content: [{ type: 'text', text }] });
|
|
58
|
+
}
|
|
59
|
+
catch (caught) {
|
|
60
|
+
return result(id, {
|
|
61
|
+
content: [{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: '错误:' + (caught instanceof Error ? caught.message : String(caught)),
|
|
64
|
+
}],
|
|
65
|
+
isError: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (method === 'resources/list') {
|
|
70
|
+
return result(id, { resources: this.listResources() });
|
|
71
|
+
}
|
|
72
|
+
if (method === 'resources/templates/list') {
|
|
73
|
+
return result(id, {
|
|
74
|
+
resourceTemplates: [{
|
|
75
|
+
uriTemplate: 'file://{path}',
|
|
76
|
+
name: 'workspace file',
|
|
77
|
+
description: '工作区内的文件',
|
|
78
|
+
mimeType: 'text/plain',
|
|
79
|
+
}],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (method === 'prompts/list') {
|
|
83
|
+
return result(id, {
|
|
84
|
+
prompts: listSkills(this.cwd).map((skill) => ({
|
|
85
|
+
name: skill.name,
|
|
86
|
+
description: skill.description,
|
|
87
|
+
arguments: [{
|
|
88
|
+
name: 'args',
|
|
89
|
+
description: '传给技能 {{args}} 的参数',
|
|
90
|
+
required: false,
|
|
91
|
+
}],
|
|
92
|
+
})),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (method === 'prompts/get') {
|
|
96
|
+
const params = message.params !== null && typeof message.params === 'object'
|
|
97
|
+
? message.params : {};
|
|
98
|
+
const name = typeof params.name === 'string' ? params.name : '';
|
|
99
|
+
const args = params.arguments !== null && typeof params.arguments === 'object'
|
|
100
|
+
? params.arguments : {};
|
|
101
|
+
const skill = readSkill(this.cwd, name, typeof args.args === 'string' ? args.args : '');
|
|
102
|
+
if (!skill) {
|
|
103
|
+
return error(id, -32602, 'prompt not found: ' + name);
|
|
104
|
+
}
|
|
105
|
+
return result(id, {
|
|
106
|
+
description: skill.description,
|
|
107
|
+
messages: [{
|
|
108
|
+
role: 'user',
|
|
109
|
+
content: {
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: '# 技能:' + skill.name + '\n\n' + skill.body,
|
|
112
|
+
},
|
|
113
|
+
}],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (method === 'resources/read') {
|
|
117
|
+
const params = message.params !== null && typeof message.params === 'object'
|
|
118
|
+
? message.params : {};
|
|
119
|
+
const uri = typeof params.uri === 'string' ? params.uri : '';
|
|
120
|
+
if (!uri.startsWith('file://')) {
|
|
121
|
+
return error(id, -32602, '只支持 file:// resource');
|
|
122
|
+
}
|
|
123
|
+
let file;
|
|
124
|
+
try {
|
|
125
|
+
file = fileURLToPath(uri);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return error(id, -32602, 'resource URI 不合法');
|
|
129
|
+
}
|
|
130
|
+
const relative = path.relative(this.cwd, file);
|
|
131
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
132
|
+
return error(id, -32602, 'resource 不在工作目录内');
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
let text = await fs.promises.readFile(file, 'utf8');
|
|
136
|
+
if (text.length > 64 * 1024) {
|
|
137
|
+
text = text.slice(0, 64 * 1024) + '\n…(已截断)';
|
|
138
|
+
}
|
|
139
|
+
return result(id, { contents: [{ uri, mimeType: 'text/plain', text }] });
|
|
140
|
+
}
|
|
141
|
+
catch (caught) {
|
|
142
|
+
return error(id, -32603, caught instanceof Error ? caught.message : String(caught));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (method === 'shutdown') {
|
|
146
|
+
return result(id, null);
|
|
147
|
+
}
|
|
148
|
+
if (method === 'exit') {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (id !== undefined && id !== null) {
|
|
152
|
+
return error(id, -32601, 'method not found: ' + method);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
listResources() {
|
|
157
|
+
const candidates = new Set();
|
|
158
|
+
for (const name of ['MINKO.md', 'AGENTS.md', 'CLAUDE.md', 'README.md', 'package.json']) {
|
|
159
|
+
const file = path.join(this.cwd, name);
|
|
160
|
+
if (fs.existsSync(file)) {
|
|
161
|
+
candidates.add(file);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const skill of listSkills(this.cwd)) {
|
|
165
|
+
if (skill.source === 'project') {
|
|
166
|
+
candidates.add(skill.path);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return [...candidates].slice(0, 50).map((file) => ({
|
|
170
|
+
uri: pathToFileURL(file).href,
|
|
171
|
+
name: path.relative(this.cwd, file).split(path.sep).join('/'),
|
|
172
|
+
description: 'Minko workspace file',
|
|
173
|
+
mimeType: file.endsWith('.json') ? 'application/json' : 'text/markdown',
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function runMcpServer(options = {}) {
|
|
178
|
+
const server = new McpServer(options);
|
|
179
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
180
|
+
for await (const line of rl) {
|
|
181
|
+
if (line.trim().length === 0) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
let message;
|
|
185
|
+
try {
|
|
186
|
+
message = JSON.parse(line);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const response = await server.handle(message);
|
|
192
|
+
if (response) {
|
|
193
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
194
|
+
}
|
|
195
|
+
if (message.method === 'exit') {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
function result(id, value) {
|
|
202
|
+
return { jsonrpc: '2.0', id, result: value };
|
|
203
|
+
}
|
|
204
|
+
function error(id, code, message) {
|
|
205
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
206
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { McpStdioClient } from "./client.js";
|
|
2
|
+
import { McpHttpClient } from "./httpClient.js";
|
|
3
|
+
/** Starts every configured MCP server and exposes its tools as normal Minko tools. */
|
|
4
|
+
export async function connectMcpServers(config) {
|
|
5
|
+
const entries = Object.entries(config ?? {}).slice(0, 20);
|
|
6
|
+
const clients = [];
|
|
7
|
+
const tools = [];
|
|
8
|
+
const errors = [];
|
|
9
|
+
await Promise.all(entries.map(async ([name, server]) => {
|
|
10
|
+
const client = server.url
|
|
11
|
+
? new McpHttpClient(name, server)
|
|
12
|
+
: new McpStdioClient(name, server);
|
|
13
|
+
try {
|
|
14
|
+
await client.start();
|
|
15
|
+
clients.push(client);
|
|
16
|
+
for (const definition of client.tools()) {
|
|
17
|
+
tools.push(bridgeTool(name, definition, client));
|
|
18
|
+
}
|
|
19
|
+
tools.push(resourceListTool(name, client));
|
|
20
|
+
tools.push(resourceReadTool(name, client));
|
|
21
|
+
tools.push(promptListTool(name, client));
|
|
22
|
+
tools.push(promptGetTool(name, client));
|
|
23
|
+
tools.push(resourceTemplateListTool(name, client));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
errors.push(name + ':' + (error instanceof Error ? error.message : String(error)));
|
|
27
|
+
await client.close().catch(() => { });
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
tools.sort((a, b) => a.spec.name.localeCompare(b.spec.name));
|
|
31
|
+
return {
|
|
32
|
+
tools,
|
|
33
|
+
errors,
|
|
34
|
+
close: async () => {
|
|
35
|
+
await Promise.all(clients.map((client) => client.close().catch(() => { })));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function bridgeTool(serverName, definition, client) {
|
|
40
|
+
const parameters = isObjectSchema(definition.inputSchema)
|
|
41
|
+
? definition.inputSchema
|
|
42
|
+
: { type: 'object', properties: {} };
|
|
43
|
+
return {
|
|
44
|
+
readOnly: false,
|
|
45
|
+
spec: {
|
|
46
|
+
name: 'mcp__' + sanitize(serverName) + '__' + sanitize(definition.name),
|
|
47
|
+
description: '[' + serverName + '] ' + (definition.description ?? definition.name),
|
|
48
|
+
parameters,
|
|
49
|
+
},
|
|
50
|
+
async run(args) {
|
|
51
|
+
return await client.callTool(definition.name, args);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function sanitize(value) {
|
|
56
|
+
return value.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
57
|
+
}
|
|
58
|
+
function resourceListTool(serverName, client) {
|
|
59
|
+
return {
|
|
60
|
+
readOnly: true,
|
|
61
|
+
spec: {
|
|
62
|
+
name: 'mcp__' + sanitize(serverName) + '__list_resources',
|
|
63
|
+
description: '[' + serverName + '] 列出 MCP server 提供的资源。',
|
|
64
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
65
|
+
},
|
|
66
|
+
async run() {
|
|
67
|
+
const resources = await client.listResources();
|
|
68
|
+
if (resources.length === 0) {
|
|
69
|
+
return '没有资源。';
|
|
70
|
+
}
|
|
71
|
+
return resources.map((resource) => resource.uri
|
|
72
|
+
+ (resource.name ? ' — ' + resource.name : '')
|
|
73
|
+
+ (resource.description ? ' — ' + resource.description : '')
|
|
74
|
+
+ (resource.mimeType ? '(' + resource.mimeType + ')' : '')).join('\n');
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function resourceReadTool(serverName, client) {
|
|
79
|
+
return {
|
|
80
|
+
readOnly: true,
|
|
81
|
+
spec: {
|
|
82
|
+
name: 'mcp__' + sanitize(serverName) + '__read_resource',
|
|
83
|
+
description: '[' + serverName + '] 读取一个 MCP resource URI 的内容。',
|
|
84
|
+
parameters: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: { uri: { type: 'string' } },
|
|
87
|
+
required: ['uri'],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
async run(args) {
|
|
92
|
+
const uri = typeof args.uri === 'string' ? args.uri.trim() : '';
|
|
93
|
+
if (uri.length === 0) {
|
|
94
|
+
throw new Error('缺少 uri 参数');
|
|
95
|
+
}
|
|
96
|
+
return await client.readResource(uri);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function promptListTool(serverName, client) {
|
|
101
|
+
return {
|
|
102
|
+
readOnly: true,
|
|
103
|
+
spec: {
|
|
104
|
+
name: 'mcp__' + sanitize(serverName) + '__list_prompts',
|
|
105
|
+
description: '[' + serverName + '] 列出 MCP prompts。',
|
|
106
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
107
|
+
},
|
|
108
|
+
async run() {
|
|
109
|
+
const prompts = await client.listPrompts();
|
|
110
|
+
if (prompts.length === 0) {
|
|
111
|
+
return '没有 prompt。';
|
|
112
|
+
}
|
|
113
|
+
return prompts.map((prompt) => prompt.name
|
|
114
|
+
+ (prompt.description ? ' — ' + prompt.description : '')
|
|
115
|
+
+ (prompt.arguments && prompt.arguments.length > 0
|
|
116
|
+
? '(参数:' + prompt.arguments.map((arg) => arg.name).join('、') + ')'
|
|
117
|
+
: '')).join('\n');
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function promptGetTool(serverName, client) {
|
|
122
|
+
return {
|
|
123
|
+
readOnly: true,
|
|
124
|
+
spec: {
|
|
125
|
+
name: 'mcp__' + sanitize(serverName) + '__get_prompt',
|
|
126
|
+
description: '[' + serverName + '] 获取一个 MCP prompt 的完整内容。',
|
|
127
|
+
parameters: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
name: { type: 'string' },
|
|
131
|
+
arguments: { type: 'object' },
|
|
132
|
+
},
|
|
133
|
+
required: ['name'],
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
async run(args) {
|
|
138
|
+
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
139
|
+
if (name.length === 0) {
|
|
140
|
+
throw new Error('缺少 name 参数');
|
|
141
|
+
}
|
|
142
|
+
const promptArgs = args.arguments !== null && typeof args.arguments === 'object'
|
|
143
|
+
? args.arguments : {};
|
|
144
|
+
return await client.getPrompt(name, promptArgs);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function resourceTemplateListTool(serverName, client) {
|
|
149
|
+
return {
|
|
150
|
+
readOnly: true,
|
|
151
|
+
spec: {
|
|
152
|
+
name: 'mcp__' + sanitize(serverName) + '__list_resource_templates',
|
|
153
|
+
description: '[' + serverName + '] 列出 MCP resource templates。',
|
|
154
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
155
|
+
},
|
|
156
|
+
async run() {
|
|
157
|
+
const templates = await client.listResourceTemplates();
|
|
158
|
+
if (templates.length === 0) {
|
|
159
|
+
return '没有 resource template。';
|
|
160
|
+
}
|
|
161
|
+
return templates.map((template) => template.uriTemplate
|
|
162
|
+
+ (template.name ? ' — ' + template.name : '')
|
|
163
|
+
+ (template.description ? ' — ' + template.description : '')).join('\n');
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function isObjectSchema(value) {
|
|
168
|
+
if (value === null || typeof value !== 'object') {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const record = value;
|
|
172
|
+
return record.type === 'object' && record.properties !== null && typeof record.properties === 'object';
|
|
173
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic OAuth 2.0 Device Authorization Grant (RFC 8628). Provider-agnostic:
|
|
3
|
+
* the endpoints and client id come from config.oauth.
|
|
4
|
+
*/
|
|
5
|
+
export async function deviceLogin(config, options = {}) {
|
|
6
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
7
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
8
|
+
const now = options.now ?? Date.now;
|
|
9
|
+
const deviceForm = new URLSearchParams({ client_id: config.clientId });
|
|
10
|
+
if (config.scopes && config.scopes.length > 0) {
|
|
11
|
+
deviceForm.set('scope', config.scopes.join(' '));
|
|
12
|
+
}
|
|
13
|
+
const device = await postForm(fetchImpl, config.deviceAuthorizationUrl, deviceForm);
|
|
14
|
+
const deviceCode = typeof device.device_code === 'string' ? device.device_code : '';
|
|
15
|
+
const userCode = typeof device.user_code === 'string' ? device.user_code : '';
|
|
16
|
+
const verificationUri = typeof device.verification_uri === 'string' ? device.verification_uri : '';
|
|
17
|
+
if (deviceCode.length === 0 || userCode.length === 0 || verificationUri.length === 0) {
|
|
18
|
+
throw new Error('OAuth 设备授权响应缺少 device_code / user_code / verification_uri');
|
|
19
|
+
}
|
|
20
|
+
const expiresIn = numberOr(device.expires_in, 600);
|
|
21
|
+
const verificationUriComplete = typeof device.verification_uri_complete === 'string'
|
|
22
|
+
? device.verification_uri_complete
|
|
23
|
+
: undefined;
|
|
24
|
+
options.onUserCode?.({
|
|
25
|
+
userCode,
|
|
26
|
+
verificationUri,
|
|
27
|
+
...(verificationUriComplete ? { verificationUriComplete } : {}),
|
|
28
|
+
expiresIn,
|
|
29
|
+
});
|
|
30
|
+
let intervalMs = Math.max(1_000, numberOr(device.interval, 5) * 1_000);
|
|
31
|
+
const deadline = now() + expiresIn * 1_000;
|
|
32
|
+
while (now() < deadline) {
|
|
33
|
+
await sleep(intervalMs);
|
|
34
|
+
const tokenForm = new URLSearchParams({
|
|
35
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
36
|
+
device_code: deviceCode,
|
|
37
|
+
client_id: config.clientId,
|
|
38
|
+
});
|
|
39
|
+
const response = await fetchImpl(config.tokenUrl, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
43
|
+
accept: 'application/json',
|
|
44
|
+
},
|
|
45
|
+
body: tokenForm,
|
|
46
|
+
});
|
|
47
|
+
const payload = await response.json().catch(() => ({}));
|
|
48
|
+
if (response.ok && typeof payload.access_token === 'string' && payload.access_token.length > 0) {
|
|
49
|
+
const token = {
|
|
50
|
+
accessToken: payload.access_token,
|
|
51
|
+
tokenType: typeof payload.token_type === 'string' ? payload.token_type : 'Bearer',
|
|
52
|
+
};
|
|
53
|
+
if (typeof payload.refresh_token === 'string') {
|
|
54
|
+
token.refreshToken = payload.refresh_token;
|
|
55
|
+
}
|
|
56
|
+
if (typeof payload.scope === 'string') {
|
|
57
|
+
token.scope = payload.scope;
|
|
58
|
+
}
|
|
59
|
+
const tokenExpiresIn = numberOr(payload.expires_in, 0);
|
|
60
|
+
if (tokenExpiresIn > 0) {
|
|
61
|
+
token.expiresAt = now() + tokenExpiresIn * 1_000;
|
|
62
|
+
}
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
const error = typeof payload.error === 'string' ? payload.error : '';
|
|
66
|
+
const description = typeof payload.error_description === 'string'
|
|
67
|
+
? payload.error_description
|
|
68
|
+
: '';
|
|
69
|
+
if (error === 'authorization_pending') {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (error === 'slow_down') {
|
|
73
|
+
intervalMs += 5_000;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (error === 'access_denied') {
|
|
77
|
+
throw new Error('OAuth 授权被拒绝' + (description ? ':' + description : ''));
|
|
78
|
+
}
|
|
79
|
+
if (error === 'expired_token') {
|
|
80
|
+
throw new Error('OAuth 设备码已过期,请重新登录');
|
|
81
|
+
}
|
|
82
|
+
if (error.length > 0) {
|
|
83
|
+
throw new Error('OAuth 登录失败:' + error + (description ? '(' + description + ')' : ''));
|
|
84
|
+
}
|
|
85
|
+
throw new Error('OAuth 令牌请求失败(HTTP ' + response.status + ')');
|
|
86
|
+
}
|
|
87
|
+
throw new Error('OAuth 登录超时');
|
|
88
|
+
}
|
|
89
|
+
async function postForm(fetchImpl, url, form) {
|
|
90
|
+
const response = await fetchImpl(url, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
},
|
|
96
|
+
body: form,
|
|
97
|
+
});
|
|
98
|
+
const payload = await response.json().catch(() => ({}));
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const error = typeof payload.error === 'string' ? payload.error : '';
|
|
101
|
+
const description = typeof payload.error_description === 'string'
|
|
102
|
+
? payload.error_description
|
|
103
|
+
: '';
|
|
104
|
+
throw new Error('OAuth 设备授权失败(HTTP ' + response.status + ')'
|
|
105
|
+
+ (error ? ':' + error : '') + (description ? '(' + description + ')' : ''));
|
|
106
|
+
}
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
109
|
+
function numberOr(value, fallback) {
|
|
110
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
111
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { minkoHome } from "../config/paths.js";
|
|
4
|
+
export function oauthStorePath() {
|
|
5
|
+
return path.join(minkoHome(), 'oauth.json');
|
|
6
|
+
}
|
|
7
|
+
export function saveOAuthToken(profile, tokens, file = oauthStorePath()) {
|
|
8
|
+
if (profile.trim().length === 0) {
|
|
9
|
+
throw new Error('OAuth profile 不能为空');
|
|
10
|
+
}
|
|
11
|
+
const store = readStore(file);
|
|
12
|
+
store.profiles[profile] = { ...tokens, savedAt: new Date().toISOString() };
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
fs.writeFileSync(file, JSON.stringify(store, null, 2) + '\n', {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
mode: 0o600,
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
fs.chmodSync(file, 0o600);
|
|
20
|
+
}
|
|
21
|
+
catch { /* Windows ignores POSIX modes */ }
|
|
22
|
+
}
|
|
23
|
+
export function loadOAuthToken(profile, file = oauthStorePath()) {
|
|
24
|
+
const token = readStore(file).profiles[profile];
|
|
25
|
+
if (!token || typeof token.accessToken !== 'string' || token.accessToken.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
accessToken: token.accessToken,
|
|
30
|
+
...(typeof token.refreshToken === 'string' ? { refreshToken: token.refreshToken } : {}),
|
|
31
|
+
tokenType: typeof token.tokenType === 'string' ? token.tokenType : 'Bearer',
|
|
32
|
+
...(typeof token.scope === 'string' ? { scope: token.scope } : {}),
|
|
33
|
+
...(typeof token.expiresAt === 'number' ? { expiresAt: token.expiresAt } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function listOAuthProfiles(file = oauthStorePath()) {
|
|
37
|
+
return Object.keys(readStore(file).profiles).sort();
|
|
38
|
+
}
|
|
39
|
+
function readStore(file) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
42
|
+
if (raw.profiles === null || typeof raw.profiles !== 'object') {
|
|
43
|
+
return { version: 1, profiles: {} };
|
|
44
|
+
}
|
|
45
|
+
const profiles = {};
|
|
46
|
+
for (const [name, value] of Object.entries(raw.profiles)) {
|
|
47
|
+
if (value !== null && typeof value === 'object') {
|
|
48
|
+
profiles[name] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { version: 1, profiles };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { version: 1, profiles: {} };
|
|
55
|
+
}
|
|
56
|
+
}
|