@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.
Files changed (189) hide show
  1. package/README.md +577 -2
  2. package/assets/mascot-preview.png +0 -0
  3. package/assets/mascot-preview.txt +46 -0
  4. package/assets/mascot-source.png +0 -0
  5. package/assets/skills/README.md +21 -0
  6. package/assets/skills/brainstorming/LICENSE +21 -0
  7. package/assets/skills/brainstorming/SKILL.md +316 -0
  8. package/assets/skills/buddhist-method/LICENSE +21 -0
  9. package/assets/skills/buddhist-method/SKILL.md +137 -0
  10. package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
  11. package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
  12. package/assets/skills/code-humanizer/LICENSE +21 -0
  13. package/assets/skills/code-humanizer/SKILL.md +174 -0
  14. package/assets/skills/code-review/LICENSE +21 -0
  15. package/assets/skills/code-review/SKILL.md +232 -0
  16. package/assets/skills/code-review/reference/angular.md +768 -0
  17. package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
  18. package/assets/skills/code-review/reference/c.md +890 -0
  19. package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
  20. package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
  21. package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
  22. package/assets/skills/code-review/reference/cpp.md +893 -0
  23. package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  24. package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  25. package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  26. package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  27. package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  28. package/assets/skills/code-review/reference/csharp.md +525 -0
  29. package/assets/skills/code-review/reference/css-less-sass.md +661 -0
  30. package/assets/skills/code-review/reference/dart.md +670 -0
  31. package/assets/skills/code-review/reference/django.md +985 -0
  32. package/assets/skills/code-review/reference/fastapi.md +580 -0
  33. package/assets/skills/code-review/reference/go.md +993 -0
  34. package/assets/skills/code-review/reference/java.md +409 -0
  35. package/assets/skills/code-review/reference/java8.md +586 -0
  36. package/assets/skills/code-review/reference/kotlin.md +1018 -0
  37. package/assets/skills/code-review/reference/nestjs.md +593 -0
  38. package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
  39. package/assets/skills/code-review/reference/php.md +684 -0
  40. package/assets/skills/code-review/reference/python.md +1073 -0
  41. package/assets/skills/code-review/reference/qt.md +757 -0
  42. package/assets/skills/code-review/reference/react.md +871 -0
  43. package/assets/skills/code-review/reference/ruby.md +964 -0
  44. package/assets/skills/code-review/reference/rust.md +846 -0
  45. package/assets/skills/code-review/reference/security-review-guide.md +494 -0
  46. package/assets/skills/code-review/reference/svelte.md +1064 -0
  47. package/assets/skills/code-review/reference/swift.md +936 -0
  48. package/assets/skills/code-review/reference/typescript.md +1016 -0
  49. package/assets/skills/code-review/reference/vue.md +924 -0
  50. package/assets/skills/code-review/reference/zig.md +440 -0
  51. package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
  52. package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
  53. package/assets/skills/five-whys/LICENSE +21 -0
  54. package/assets/skills/five-whys/SKILL.md +247 -0
  55. package/assets/skills/five-whys/examples/performance-regression.md +161 -0
  56. package/assets/skills/five-whys/examples/production-incident.md +134 -0
  57. package/assets/skills/five-whys/references/software-patterns.md +284 -0
  58. package/assets/skills/five-whys/references/toyota-origins.md +257 -0
  59. package/assets/skills/systematic-debugging/LICENSE +21 -0
  60. package/assets/skills/systematic-debugging/SKILL.md +283 -0
  61. package/assets/skills/test-driven-development/LICENSE +21 -0
  62. package/assets/skills/test-driven-development/SKILL.md +330 -0
  63. package/assets/skills/verification-before-completion/LICENSE +21 -0
  64. package/assets/skills/verification-before-completion/SKILL.md +120 -0
  65. package/assets/skills/vibe-sec/LICENSE +201 -0
  66. package/assets/skills/vibe-sec/SKILL.md +758 -0
  67. package/assets/skills/writing-plans/LICENSE +21 -0
  68. package/assets/skills/writing-plans/SKILL.md +204 -0
  69. package/bin/minko.js +2 -2
  70. package/dist/agent/approvalTokens.js +224 -0
  71. package/dist/agent/branchLock.js +315 -0
  72. package/dist/agent/compact.js +182 -0
  73. package/dist/agent/context.js +183 -0
  74. package/dist/agent/cron.js +326 -0
  75. package/dist/agent/hooks.js +211 -0
  76. package/dist/agent/lanes.js +511 -0
  77. package/dist/agent/loop.js +337 -0
  78. package/dist/agent/memory.js +159 -0
  79. package/dist/agent/permissions.js +71 -0
  80. package/dist/agent/plan.js +118 -0
  81. package/dist/agent/prompt.js +120 -0
  82. package/dist/agent/subagent.js +67 -0
  83. package/dist/agent/tasks.js +449 -0
  84. package/dist/agent/transcript.js +62 -0
  85. package/dist/agent/workerStore.js +172 -0
  86. package/dist/agent/workerTypes.js +1 -0
  87. package/dist/agent/workers.js +503 -0
  88. package/dist/cli/app.js +261 -0
  89. package/dist/cli/argv.js +206 -0
  90. package/dist/cli/commandsCommand.js +12 -0
  91. package/dist/cli/configCommand.js +170 -0
  92. package/dist/cli/doctorCommand.js +112 -0
  93. package/dist/cli/exportCommand.js +85 -0
  94. package/dist/cli/forkCommand.js +24 -0
  95. package/dist/cli/linkCommand.js +272 -0
  96. package/dist/cli/loginCommand.js +69 -0
  97. package/dist/cli/mcpLoginCommand.js +68 -0
  98. package/dist/cli/pluginCommand.js +113 -0
  99. package/dist/cli/powerCommand.js +174 -0
  100. package/dist/cli/promptFooter.js +74 -0
  101. package/dist/cli/relayCommand.js +38 -0
  102. package/dist/cli/sandboxCommand.js +31 -0
  103. package/dist/cli/serveCommand.js +104 -0
  104. package/dist/cli/statusCommand.js +120 -0
  105. package/dist/cli/tui.js +1104 -0
  106. package/dist/commands/registry.js +99 -0
  107. package/dist/config/paths.js +81 -0
  108. package/dist/config/store.js +605 -0
  109. package/dist/index.js +199 -0
  110. package/dist/link/protocol.js +159 -0
  111. package/dist/link/relay.js +184 -0
  112. package/dist/link/server.js +566 -0
  113. package/dist/link/tunnel.js +90 -0
  114. package/dist/lsp/client.js +340 -0
  115. package/dist/lsp/tool.js +177 -0
  116. package/dist/mcp/client.js +469 -0
  117. package/dist/mcp/httpClient.js +191 -0
  118. package/dist/mcp/server.js +206 -0
  119. package/dist/mcp/tools.js +173 -0
  120. package/dist/oauth/device.js +111 -0
  121. package/dist/oauth/store.js +56 -0
  122. package/dist/plugins/loader.js +291 -0
  123. package/dist/plugins/market.js +98 -0
  124. package/dist/providers/anthropic.js +279 -0
  125. package/dist/providers/factory.js +109 -0
  126. package/dist/providers/openai.js +182 -0
  127. package/dist/providers/retry.js +95 -0
  128. package/dist/providers/sse.js +67 -0
  129. package/dist/providers/types.js +10 -0
  130. package/dist/rag/embeddings.js +171 -0
  131. package/dist/rag/index.js +373 -0
  132. package/dist/rag/server.js +167 -0
  133. package/dist/rag/tool.js +148 -0
  134. package/dist/sandbox/index.js +55 -0
  135. package/dist/sdk/server.js +150 -0
  136. package/dist/sdk/session.js +104 -0
  137. package/dist/session/liveness.js +148 -0
  138. package/dist/session/recorder.js +49 -0
  139. package/dist/session/store.js +171 -0
  140. package/dist/skills/index.js +428 -0
  141. package/dist/theme.js +30 -0
  142. package/dist/tools/fs.js +195 -0
  143. package/dist/tools/notebook.js +109 -0
  144. package/dist/tools/registry.js +32 -0
  145. package/dist/tools/search.js +130 -0
  146. package/dist/tools/shell.js +200 -0
  147. package/dist/tools/structured.js +83 -0
  148. package/dist/tools/types.js +1 -0
  149. package/dist/ui/ansi.js +135 -0
  150. package/dist/ui/banner.js +103 -0
  151. package/dist/ui/input.js +49 -0
  152. package/dist/ui/inputModel.js +113 -0
  153. package/dist/ui/keyboard.js +142 -0
  154. package/dist/ui/mascot.js +34 -0
  155. package/dist/ui/mascotData.js +96 -0
  156. package/dist/ui/md.js +183 -0
  157. package/dist/ui/screen.js +49 -0
  158. package/dist/ui/spinner.js +52 -0
  159. package/dist/ui/transcript.js +287 -0
  160. package/dist/util/clipboard.js +53 -0
  161. package/dist/util/errors.js +50 -0
  162. package/dist/util/version.js +12 -0
  163. package/dist/web/fetch.js +168 -0
  164. package/dist/web/search.js +97 -0
  165. package/package.json +45 -36
  166. package/src/agent/loop.ts +0 -159
  167. package/src/agent/prompt.ts +0 -45
  168. package/src/cli/app.ts +0 -396
  169. package/src/cli/argv.ts +0 -139
  170. package/src/cli/configCommand.ts +0 -175
  171. package/src/config/paths.ts +0 -20
  172. package/src/config/store.ts +0 -149
  173. package/src/index.ts +0 -120
  174. package/src/providers/anthropic.ts +0 -215
  175. package/src/providers/openai.ts +0 -158
  176. package/src/providers/sse.ts +0 -57
  177. package/src/providers/types.ts +0 -59
  178. package/src/session/store.ts +0 -125
  179. package/src/theme.ts +0 -41
  180. package/src/tools/fs.ts +0 -143
  181. package/src/tools/registry.ts +0 -19
  182. package/src/tools/shell.ts +0 -120
  183. package/src/tools/types.ts +0 -26
  184. package/src/ui/ansi.ts +0 -110
  185. package/src/ui/banner.ts +0 -39
  186. package/src/ui/input.ts +0 -54
  187. package/src/ui/md.ts +0 -160
  188. package/src/ui/spinner.ts +0 -58
  189. 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
+ }