@llmindset/hf-mcp 0.2.32 → 0.2.34
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/dist/docs-search/docs-semantic-search.d.ts +2 -2
- package/dist/docs-search/docs-semantic-search.d.ts.map +1 -1
- package/dist/docs-search/docs-semantic-search.js +56 -21
- package/dist/docs-search/docs-semantic-search.js.map +1 -1
- package/dist/hf-api-call.d.ts.map +1 -1
- package/dist/hf-api-call.js +4 -0
- package/dist/hf-api-call.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jobs/api-client.d.ts +19 -0
- package/dist/jobs/api-client.d.ts.map +1 -0
- package/dist/jobs/api-client.js +104 -0
- package/dist/jobs/api-client.js.map +1 -0
- package/dist/jobs/commands/inspect.d.ts +5 -0
- package/dist/jobs/commands/inspect.d.ts.map +1 -0
- package/dist/jobs/commands/inspect.js +21 -0
- package/dist/jobs/commands/inspect.js.map +1 -0
- package/dist/jobs/commands/logs.d.ts +4 -0
- package/dist/jobs/commands/logs.d.ts.map +1 -0
- package/dist/jobs/commands/logs.js +24 -0
- package/dist/jobs/commands/logs.js.map +1 -0
- package/dist/jobs/commands/ps.d.ts +4 -0
- package/dist/jobs/commands/ps.d.ts.map +1 -0
- package/dist/jobs/commands/ps.js +23 -0
- package/dist/jobs/commands/ps.js.map +1 -0
- package/dist/jobs/commands/run.d.ts +5 -0
- package/dist/jobs/commands/run.d.ts.map +1 -0
- package/dist/jobs/commands/run.js +89 -0
- package/dist/jobs/commands/run.js.map +1 -0
- package/dist/jobs/commands/scheduled.d.ts +10 -0
- package/dist/jobs/commands/scheduled.d.ts.map +1 -0
- package/dist/jobs/commands/scheduled.js +111 -0
- package/dist/jobs/commands/scheduled.js.map +1 -0
- package/dist/jobs/commands/utils.d.ts +19 -0
- package/dist/jobs/commands/utils.d.ts.map +1 -0
- package/dist/jobs/commands/utils.js +99 -0
- package/dist/jobs/commands/utils.js.map +1 -0
- package/dist/jobs/formatters.d.ts +6 -0
- package/dist/jobs/formatters.d.ts.map +1 -0
- package/dist/jobs/formatters.js +98 -0
- package/dist/jobs/formatters.js.map +1 -0
- package/dist/jobs/sse-handler.d.ts +12 -0
- package/dist/jobs/sse-handler.d.ts.map +1 -0
- package/dist/jobs/sse-handler.js +80 -0
- package/dist/jobs/sse-handler.js.map +1 -0
- package/dist/jobs/tool.d.ts +35 -0
- package/dist/jobs/tool.d.ts.map +1 -0
- package/dist/jobs/tool.js +333 -0
- package/dist/jobs/tool.js.map +1 -0
- package/dist/jobs/types.d.ts +295 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +95 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/tool-ids.d.ts +3 -2
- package/dist/tool-ids.d.ts.map +1 -1
- package/dist/tool-ids.js +10 -2
- package/dist/tool-ids.js.map +1 -1
- package/dist/types/tool-result.d.ts +1 -0
- package/dist/types/tool-result.d.ts.map +1 -1
- package/dist/use-space.d.ts +0 -1
- package/dist/use-space.d.ts.map +1 -1
- package/dist/use-space.js +0 -1
- package/dist/use-space.js.map +1 -1
- package/package.json +4 -2
- package/src/docs-search/docs-semantic-search.ts +71 -20
- package/src/hf-api-call.ts +6 -0
- package/src/index.ts +1 -0
- package/src/jobs/api-client.ts +195 -0
- package/src/jobs/commands/inspect.ts +38 -0
- package/src/jobs/commands/logs.ts +36 -0
- package/src/jobs/commands/ps.ts +40 -0
- package/src/jobs/commands/run.ts +134 -0
- package/src/jobs/commands/scheduled.ts +189 -0
- package/src/jobs/commands/utils.ts +158 -0
- package/src/jobs/formatters.ts +149 -0
- package/src/jobs/sse-handler.ts +144 -0
- package/src/jobs/tool.ts +435 -0
- package/src/jobs/types.ts +237 -0
- package/src/tool-ids.ts +11 -1
- package/src/types/tool-result.ts +6 -0
- package/src/use-space.ts +0 -1
- package/test/jobs/command-translation.spec.ts +277 -0
- package/test/jobs/formatters.spec.ts +267 -0
- package/test/jobs/uv-command.spec.ts +81 -0
- package/dist/types/mcp-ui-server-shim.d.ts +0 -37
- package/dist/types/mcp-ui-server-shim.d.ts.map +0 -1
- package/dist/types/mcp-ui-server-shim.js +0 -2
- package/dist/types/mcp-ui-server-shim.js.map +0 -1
- package/src/types/mcp-ui-server-shim.ts +0 -35
package/src/types/tool-result.ts
CHANGED
|
@@ -21,4 +21,10 @@ export interface ToolResult {
|
|
|
21
21
|
* For prompts: 1 if generated successfully
|
|
22
22
|
*/
|
|
23
23
|
resultsShared: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Indicates whether this result represents an error condition
|
|
27
|
+
* When true, formatted contains an error message
|
|
28
|
+
*/
|
|
29
|
+
isError?: boolean;
|
|
24
30
|
}
|
package/src/use-space.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { z } from 'zod';
|
|
|
2
2
|
import { HfApiCall } from './hf-api-call.js';
|
|
3
3
|
import { spaceInfo, type SpaceEntry } from '@huggingface/hub';
|
|
4
4
|
import type { ToolResult } from './types/tool-result.js';
|
|
5
|
-
import './types/mcp-ui-server-shim.js';
|
|
6
5
|
import { createUIResource } from '@mcp-ui/server';
|
|
7
6
|
// Define the return type that matches MCP server expectations
|
|
8
7
|
interface UseSpaceResult {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseTimeout, parseImageSource, parseCommand, createJobSpec } from '../../src/jobs/commands/utils.js';
|
|
3
|
+
|
|
4
|
+
describe('Jobs Command Translation', () => {
|
|
5
|
+
describe('parseTimeout', () => {
|
|
6
|
+
it('should parse timeout with seconds', () => {
|
|
7
|
+
expect(parseTimeout('30s')).toBe(30);
|
|
8
|
+
expect(parseTimeout('45s')).toBe(45);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should parse timeout with minutes', () => {
|
|
12
|
+
expect(parseTimeout('5m')).toBe(300);
|
|
13
|
+
expect(parseTimeout('10m')).toBe(600);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should parse timeout with hours', () => {
|
|
17
|
+
expect(parseTimeout('2h')).toBe(7200);
|
|
18
|
+
expect(parseTimeout('1h')).toBe(3600);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse timeout with days', () => {
|
|
22
|
+
expect(parseTimeout('1d')).toBe(86400);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should parse plain number as seconds', () => {
|
|
26
|
+
expect(parseTimeout('300')).toBe(300);
|
|
27
|
+
expect(parseTimeout('60')).toBe(60);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle decimal values', () => {
|
|
31
|
+
expect(parseTimeout('1.5h')).toBe(5400);
|
|
32
|
+
expect(parseTimeout('2.5m')).toBe(150);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should throw error for invalid format', () => {
|
|
36
|
+
expect(() => parseTimeout('invalid')).toThrow(/Invalid timeout format/);
|
|
37
|
+
expect(() => parseTimeout('xyz')).toThrow(/Invalid timeout format/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should parse partial numbers (parseInt behavior)', () => {
|
|
41
|
+
// parseInt stops at first non-numeric, so '5x' becomes 5
|
|
42
|
+
expect(parseTimeout('5x')).toBe(5);
|
|
43
|
+
expect(parseTimeout('10abc')).toBe(10);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('parseImageSource', () => {
|
|
48
|
+
it('should detect Docker Hub images', () => {
|
|
49
|
+
const result = parseImageSource('python:3.12');
|
|
50
|
+
expect(result.dockerImage).toBe('python:3.12');
|
|
51
|
+
expect(result.spaceId).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should detect HuggingFace Space URLs with https://huggingface.co/', () => {
|
|
55
|
+
const result = parseImageSource('https://huggingface.co/spaces/user/app');
|
|
56
|
+
expect(result.spaceId).toBe('user/app');
|
|
57
|
+
expect(result.dockerImage).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should detect HuggingFace Space URLs with https://hf.co/', () => {
|
|
61
|
+
const result = parseImageSource('https://hf.co/spaces/user/app');
|
|
62
|
+
expect(result.spaceId).toBe('user/app');
|
|
63
|
+
expect(result.dockerImage).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should detect HuggingFace Space URLs without https prefix', () => {
|
|
67
|
+
const result = parseImageSource('huggingface.co/spaces/user/app');
|
|
68
|
+
expect(result.spaceId).toBe('user/app');
|
|
69
|
+
expect(result.dockerImage).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should detect HuggingFace Space URLs with hf.co shorthand', () => {
|
|
73
|
+
const result = parseImageSource('hf.co/spaces/user/app');
|
|
74
|
+
expect(result.spaceId).toBe('user/app');
|
|
75
|
+
expect(result.dockerImage).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should treat complex docker images as docker images', () => {
|
|
79
|
+
const result = parseImageSource('ghcr.io/owner/repo:tag');
|
|
80
|
+
expect(result.dockerImage).toBe('ghcr.io/owner/repo:tag');
|
|
81
|
+
expect(result.spaceId).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('parseCommand', () => {
|
|
86
|
+
it('should return array commands as-is', () => {
|
|
87
|
+
const result = parseCommand(['python', '-c', 'print("hello")']);
|
|
88
|
+
expect(result.command).toEqual(['python', '-c', 'print("hello")']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should split simple string commands', () => {
|
|
92
|
+
const result = parseCommand('python script.py');
|
|
93
|
+
expect(result.command).toEqual(['python', 'script.py']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle single word commands', () => {
|
|
97
|
+
const result = parseCommand('ls');
|
|
98
|
+
expect(result.command).toEqual(['ls']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle double-quoted strings', () => {
|
|
102
|
+
const result = parseCommand('python -c "print(\'Hello world!\')"');
|
|
103
|
+
expect(result.command).toEqual(['python', '-c', "print('Hello world!')"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle single-quoted strings', () => {
|
|
107
|
+
const result = parseCommand("python -c 'print(\"Hello world!\")'");
|
|
108
|
+
expect(result.command).toEqual(['python', '-c', 'print("Hello world!")']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle escaped quotes', () => {
|
|
112
|
+
const result = parseCommand('echo "He said \\"hello\\""');
|
|
113
|
+
expect(result.command).toEqual(['echo', 'He said "hello"']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should handle mixed quotes and spaces', () => {
|
|
117
|
+
const result = parseCommand('python -c "print(\'hello world\')"');
|
|
118
|
+
expect(result.command).toEqual(['python', '-c', "print('hello world')"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle multiple quoted arguments', () => {
|
|
122
|
+
const result = parseCommand('cmd "arg one" "arg two" "arg three"');
|
|
123
|
+
expect(result.command).toEqual(['cmd', 'arg one', 'arg two', 'arg three']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle quoted strings with no spaces', () => {
|
|
127
|
+
const result = parseCommand('echo "hello"');
|
|
128
|
+
expect(result.command).toEqual(['echo', 'hello']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle empty quotes', () => {
|
|
132
|
+
const result = parseCommand('echo "" test');
|
|
133
|
+
expect(result.command).toEqual(['echo', '', 'test']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle real-world example from docs', () => {
|
|
137
|
+
const result = parseCommand('python -c "print(\'Hello world!\')"');
|
|
138
|
+
expect(result.command).toEqual(['python', '-c', "print('Hello world!')"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle commands with multiple spaces', () => {
|
|
142
|
+
const result = parseCommand('python script.py --arg');
|
|
143
|
+
expect(result.command).toEqual(['python', 'script.py', '--arg']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle commands with tabs', () => {
|
|
147
|
+
const result = parseCommand('python\tscript.py\t--arg');
|
|
148
|
+
expect(result.command).toEqual(['python', 'script.py', '--arg']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle mixed whitespace', () => {
|
|
152
|
+
const result = parseCommand('cmd \t arg1\t\t arg2');
|
|
153
|
+
expect(result.command).toEqual(['cmd', 'arg1', 'arg2']);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle backslash escaping (POSIX shell semantics)', () => {
|
|
157
|
+
// In POSIX shells, \n in double quotes is literal, not a newline
|
|
158
|
+
const result = parseCommand('echo "hello\\nworld"');
|
|
159
|
+
expect(result.command).toEqual(['echo', 'hello\\nworld']);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle backslash literally in single quotes', () => {
|
|
163
|
+
const result = parseCommand("echo 'hello\\nworld'");
|
|
164
|
+
expect(result.command).toEqual(['echo', 'hello\\nworld']);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should throw error for shell operators', () => {
|
|
168
|
+
expect(() => parseCommand('echo hello | grep world')).toThrow(/Unsupported shell syntax/);
|
|
169
|
+
expect(() => parseCommand('ls && pwd')).toThrow(/Unsupported shell syntax/);
|
|
170
|
+
expect(() => parseCommand('cat file > output.txt')).toThrow(/Unsupported shell syntax/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw error for empty command', () => {
|
|
174
|
+
expect(() => parseCommand('')).toThrow(/Command cannot be empty/);
|
|
175
|
+
expect(() => parseCommand(' ')).toThrow(/Command cannot be empty/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle environment variables as literal references', () => {
|
|
179
|
+
// shell-quote parses $HOME as an env token, which we format as literal string
|
|
180
|
+
// This preserves the variable reference for the job runtime to handle
|
|
181
|
+
const result = parseCommand('echo $HOME');
|
|
182
|
+
expect(result.command).toEqual(['echo', '$HOME']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should preserve complex environment variable syntax', () => {
|
|
186
|
+
const result = parseCommand('echo ${FOO:-bar} ${BAR?missing}');
|
|
187
|
+
expect(result.command).toEqual(['echo', '${FOO:-bar}', '${BAR?missing}']);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should retain special parameter references', () => {
|
|
191
|
+
const result = parseCommand('echo $$ $1 $@ $* $- $! $_');
|
|
192
|
+
expect(result.command).toEqual(['echo', '$$', '$1', '$@', '$*', '$-', '$!', '$_']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should treat escaped dollars as literal variables', () => {
|
|
196
|
+
const result = parseCommand('echo \\$FOO "$BAR"');
|
|
197
|
+
expect(result.command).toEqual(['echo', '$FOO', '$BAR']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle multiline Python in array format', () => {
|
|
201
|
+
const result = parseCommand(['python', '-c', 'import sys\nprint("hello")\nprint("world")']);
|
|
202
|
+
expect(result.command).toEqual(['python', '-c', 'import sys\nprint("hello")\nprint("world")']);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('createJobSpec', () => {
|
|
207
|
+
it('should create a basic job spec', () => {
|
|
208
|
+
const spec = createJobSpec({
|
|
209
|
+
image: 'python:3.12',
|
|
210
|
+
command: 'python -c "print(123)"',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(spec.dockerImage).toBe('python:3.12');
|
|
214
|
+
expect(spec.command).toEqual(['python', '-c', 'print(123)']);
|
|
215
|
+
expect(spec.flavor).toBe('cpu-basic');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle Space URLs', () => {
|
|
219
|
+
const spec = createJobSpec({
|
|
220
|
+
image: 'hf.co/spaces/user/app',
|
|
221
|
+
command: 'python run.py',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(spec.spaceId).toBe('user/app');
|
|
225
|
+
expect(spec.dockerImage).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should include environment variables', () => {
|
|
229
|
+
const spec = createJobSpec({
|
|
230
|
+
image: 'python:3.12',
|
|
231
|
+
command: 'python script.py',
|
|
232
|
+
env: { FOO: 'bar', BAZ: 'qux' },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(spec.environment).toEqual({ FOO: 'bar', BAZ: 'qux' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should include secrets', () => {
|
|
239
|
+
const spec = createJobSpec({
|
|
240
|
+
image: 'python:3.12',
|
|
241
|
+
command: 'python script.py',
|
|
242
|
+
secrets: { API_KEY: 'secret123' },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(spec.secrets).toEqual({ API_KEY: 'secret123' });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should parse and include timeout', () => {
|
|
249
|
+
const spec = createJobSpec({
|
|
250
|
+
image: 'python:3.12',
|
|
251
|
+
command: 'python script.py',
|
|
252
|
+
timeout: '5m',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(spec.timeoutSeconds).toBe(300);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should use specified flavor', () => {
|
|
259
|
+
const spec = createJobSpec({
|
|
260
|
+
image: 'python:3.12',
|
|
261
|
+
command: 'python script.py',
|
|
262
|
+
flavor: 'a10g-small',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(spec.flavor).toBe('a10g-small');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should handle array commands', () => {
|
|
269
|
+
const spec = createJobSpec({
|
|
270
|
+
image: 'ubuntu',
|
|
271
|
+
command: ['bash', '-c', 'echo hello'],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(spec.command).toEqual(['bash', '-c', 'echo hello']);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatJobsTable, formatScheduledJobsTable, formatJobDetails } from '../../src/jobs/formatters.js';
|
|
3
|
+
import type { JobInfo, ScheduledJobInfo, JobSpec } from '../../src/jobs/types.js';
|
|
4
|
+
|
|
5
|
+
describe('Jobs Formatters', () => {
|
|
6
|
+
describe('formatJobsTable', () => {
|
|
7
|
+
it('should return message for empty job list', () => {
|
|
8
|
+
const result = formatJobsTable([]);
|
|
9
|
+
expect(result).toBe('No jobs found.');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should format a single job as markdown table', () => {
|
|
13
|
+
const jobs: JobInfo[] = [
|
|
14
|
+
{
|
|
15
|
+
id: 'job123',
|
|
16
|
+
createdAt: '2025-01-20T10:00:00Z',
|
|
17
|
+
dockerImage: 'python:3.12',
|
|
18
|
+
command: ['python', 'script.py'],
|
|
19
|
+
flavor: 'cpu-basic',
|
|
20
|
+
status: { stage: 'RUNNING' },
|
|
21
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
22
|
+
environment: {},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const result = formatJobsTable(jobs);
|
|
27
|
+
|
|
28
|
+
// Should be a markdown table
|
|
29
|
+
expect(result).toContain('| JOB ID');
|
|
30
|
+
expect(result).toContain('| IMAGE/SPACE');
|
|
31
|
+
expect(result).toContain('| COMMAND');
|
|
32
|
+
expect(result).toContain('| CREATED');
|
|
33
|
+
expect(result).toContain('| STATUS');
|
|
34
|
+
|
|
35
|
+
// Should contain separator line
|
|
36
|
+
expect(result).toContain('|---');
|
|
37
|
+
|
|
38
|
+
// Should contain job data
|
|
39
|
+
expect(result).toContain('job123');
|
|
40
|
+
expect(result).toContain('python:3.12');
|
|
41
|
+
expect(result).toContain('python script.py');
|
|
42
|
+
expect(result).toContain('RUNNING');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should format multiple jobs', () => {
|
|
46
|
+
const jobs: JobInfo[] = [
|
|
47
|
+
{
|
|
48
|
+
id: 'job123',
|
|
49
|
+
createdAt: '2025-01-20T10:00:00Z',
|
|
50
|
+
dockerImage: 'python:3.12',
|
|
51
|
+
command: ['python', 'script.py'],
|
|
52
|
+
flavor: 'cpu-basic',
|
|
53
|
+
status: { stage: 'RUNNING' },
|
|
54
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
55
|
+
environment: {},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'job456',
|
|
59
|
+
createdAt: '2025-01-20T11:00:00Z',
|
|
60
|
+
dockerImage: 'ubuntu',
|
|
61
|
+
command: ['bash', 'test.sh'],
|
|
62
|
+
flavor: 'a10g-small',
|
|
63
|
+
status: { stage: 'COMPLETED' },
|
|
64
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
65
|
+
environment: {},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const result = formatJobsTable(jobs);
|
|
70
|
+
|
|
71
|
+
// Should contain both jobs
|
|
72
|
+
expect(result).toContain('job123');
|
|
73
|
+
expect(result).toContain('job456');
|
|
74
|
+
expect(result).toContain('python:3.12');
|
|
75
|
+
expect(result).toContain('ubuntu');
|
|
76
|
+
expect(result).toContain('RUNNING');
|
|
77
|
+
expect(result).toContain('COMPLETED');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle Space IDs instead of Docker images', () => {
|
|
81
|
+
const jobs: JobInfo[] = [
|
|
82
|
+
{
|
|
83
|
+
id: 'job789',
|
|
84
|
+
createdAt: '2025-01-20T12:00:00Z',
|
|
85
|
+
spaceId: 'user/myspace',
|
|
86
|
+
command: ['python', 'app.py'],
|
|
87
|
+
flavor: 'cpu-basic',
|
|
88
|
+
status: { stage: 'RUNNING' },
|
|
89
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
90
|
+
environment: {},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const result = formatJobsTable(jobs);
|
|
95
|
+
expect(result).toContain('user/myspace');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should truncate long values with ellipsis', () => {
|
|
99
|
+
const jobs: JobInfo[] = [
|
|
100
|
+
{
|
|
101
|
+
id: 'a'.repeat(50),
|
|
102
|
+
createdAt: '2025-01-20T10:00:00Z',
|
|
103
|
+
dockerImage: 'very-long-image-name-that-exceeds-column-width',
|
|
104
|
+
command: ['python', '-c', 'print(' + 'x'.repeat(100) + ')'],
|
|
105
|
+
flavor: 'cpu-basic',
|
|
106
|
+
status: { stage: 'RUNNING' },
|
|
107
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
108
|
+
environment: {},
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const result = formatJobsTable(jobs);
|
|
113
|
+
expect(result).toContain('...');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('formatScheduledJobsTable', () => {
|
|
118
|
+
it('should return message for empty scheduled job list', () => {
|
|
119
|
+
const result = formatScheduledJobsTable([]);
|
|
120
|
+
expect(result).toBe('No scheduled jobs found.');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should format a scheduled job as markdown table', () => {
|
|
124
|
+
const jobSpec: JobSpec = {
|
|
125
|
+
dockerImage: 'python:3.12',
|
|
126
|
+
command: ['python', 'backup.py'],
|
|
127
|
+
flavor: 'cpu-basic',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const jobs: ScheduledJobInfo[] = [
|
|
131
|
+
{
|
|
132
|
+
id: 'sched123',
|
|
133
|
+
schedule: '@hourly',
|
|
134
|
+
suspend: false,
|
|
135
|
+
jobSpec,
|
|
136
|
+
lastRun: '2025-01-20T10:00:00Z',
|
|
137
|
+
nextRun: '2025-01-20T11:00:00Z',
|
|
138
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
139
|
+
createdAt: '2025-01-20T09:00:00Z',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const result = formatScheduledJobsTable(jobs);
|
|
144
|
+
|
|
145
|
+
// Should be a markdown table
|
|
146
|
+
expect(result).toContain('| ID');
|
|
147
|
+
expect(result).toContain('| SCHEDULE');
|
|
148
|
+
expect(result).toContain('| IMAGE/SPACE');
|
|
149
|
+
expect(result).toContain('| COMMAND');
|
|
150
|
+
expect(result).toContain('| LAST RUN');
|
|
151
|
+
expect(result).toContain('| NEXT RUN');
|
|
152
|
+
expect(result).toContain('| SUSPENDED');
|
|
153
|
+
|
|
154
|
+
// Should contain job data
|
|
155
|
+
expect(result).toContain('sched123');
|
|
156
|
+
expect(result).toContain('@hourly');
|
|
157
|
+
expect(result).toContain('python:3.12');
|
|
158
|
+
expect(result).toContain('No'); // Not suspended
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should show Yes for suspended jobs', () => {
|
|
162
|
+
const jobSpec: JobSpec = {
|
|
163
|
+
dockerImage: 'ubuntu',
|
|
164
|
+
command: ['bash', 'cleanup.sh'],
|
|
165
|
+
flavor: 'cpu-basic',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const jobs: ScheduledJobInfo[] = [
|
|
169
|
+
{
|
|
170
|
+
id: 'sched456',
|
|
171
|
+
schedule: '@daily',
|
|
172
|
+
suspend: true,
|
|
173
|
+
jobSpec,
|
|
174
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
175
|
+
createdAt: '2025-01-20T09:00:00Z',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const result = formatScheduledJobsTable(jobs);
|
|
180
|
+
expect(result).toContain('Yes'); // Suspended
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('formatJobDetails', () => {
|
|
185
|
+
it('should format a single job as JSON in code block', () => {
|
|
186
|
+
const job: JobInfo = {
|
|
187
|
+
id: 'job123',
|
|
188
|
+
createdAt: '2025-01-20T10:00:00Z',
|
|
189
|
+
dockerImage: 'python:3.12',
|
|
190
|
+
command: ['python', 'script.py'],
|
|
191
|
+
flavor: 'cpu-basic',
|
|
192
|
+
status: { stage: 'RUNNING', message: null },
|
|
193
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
194
|
+
environment: {},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = formatJobDetails(job);
|
|
198
|
+
|
|
199
|
+
// Should be wrapped in code block
|
|
200
|
+
expect(result).toMatch(/^```json\n/);
|
|
201
|
+
expect(result).toMatch(/\n```$/);
|
|
202
|
+
|
|
203
|
+
// Should contain job data as JSON
|
|
204
|
+
expect(result).toContain('"id": "job123"');
|
|
205
|
+
expect(result).toContain('"dockerImage": "python:3.12"');
|
|
206
|
+
expect(result).toContain('"stage": "RUNNING"');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should format multiple jobs as JSON array', () => {
|
|
210
|
+
const jobs: JobInfo[] = [
|
|
211
|
+
{
|
|
212
|
+
id: 'job123',
|
|
213
|
+
createdAt: '2025-01-20T10:00:00Z',
|
|
214
|
+
dockerImage: 'python:3.12',
|
|
215
|
+
command: ['python', 'script.py'],
|
|
216
|
+
flavor: 'cpu-basic',
|
|
217
|
+
status: { stage: 'RUNNING' },
|
|
218
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
219
|
+
environment: {},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'job456',
|
|
223
|
+
createdAt: '2025-01-20T11:00:00Z',
|
|
224
|
+
dockerImage: 'ubuntu',
|
|
225
|
+
command: ['bash', 'test.sh'],
|
|
226
|
+
flavor: 'cpu-basic',
|
|
227
|
+
status: { stage: 'COMPLETED' },
|
|
228
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
229
|
+
environment: {},
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const result = formatJobDetails(jobs);
|
|
234
|
+
|
|
235
|
+
// Should be wrapped in code block
|
|
236
|
+
expect(result).toMatch(/^```json\n/);
|
|
237
|
+
expect(result).toMatch(/\n```$/);
|
|
238
|
+
|
|
239
|
+
// Should be an array
|
|
240
|
+
expect(result).toContain('[');
|
|
241
|
+
expect(result).toContain(']');
|
|
242
|
+
|
|
243
|
+
// Should contain both jobs
|
|
244
|
+
expect(result).toContain('"id": "job123"');
|
|
245
|
+
expect(result).toContain('"id": "job456"');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should format JSON with proper indentation', () => {
|
|
249
|
+
const job: JobInfo = {
|
|
250
|
+
id: 'job123',
|
|
251
|
+
createdAt: '2025-01-20T10:00:00Z',
|
|
252
|
+
dockerImage: 'python:3.12',
|
|
253
|
+
command: ['python', 'script.py'],
|
|
254
|
+
flavor: 'cpu-basic',
|
|
255
|
+
status: { stage: 'RUNNING' },
|
|
256
|
+
owner: { id: 'u123', name: 'testuser', type: 'user' },
|
|
257
|
+
environment: {},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = formatJobDetails(job);
|
|
261
|
+
|
|
262
|
+
// Should have proper indentation (2 spaces)
|
|
263
|
+
expect(result).toContain(' "id"');
|
|
264
|
+
expect(result).toContain(' "createdAt"');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { uvCommand } from '../../src/jobs/commands/run.js';
|
|
3
|
+
import type { JobsApiClient } from '../../src/jobs/api-client.js';
|
|
4
|
+
import type { JobInfo, JobSpec } from '../../src/jobs/types.js';
|
|
5
|
+
|
|
6
|
+
function setupMockClient() {
|
|
7
|
+
let capturedSpec: JobSpec | undefined;
|
|
8
|
+
|
|
9
|
+
const runJob = vi.fn(async (spec: JobSpec) => {
|
|
10
|
+
capturedSpec = spec;
|
|
11
|
+
const job: JobInfo = {
|
|
12
|
+
id: 'job-123',
|
|
13
|
+
createdAt: new Date().toISOString(),
|
|
14
|
+
command: spec.command,
|
|
15
|
+
arguments: spec.arguments,
|
|
16
|
+
environment: spec.environment ?? {},
|
|
17
|
+
secrets: {},
|
|
18
|
+
flavor: spec.flavor,
|
|
19
|
+
status: { stage: 'RUNNING' },
|
|
20
|
+
owner: { id: 'owner-1', name: 'tester', type: 'user' },
|
|
21
|
+
};
|
|
22
|
+
return job;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const getLogsUrl = vi.fn(() => 'https://example.test/logs');
|
|
26
|
+
|
|
27
|
+
const client = {
|
|
28
|
+
runJob,
|
|
29
|
+
getLogsUrl,
|
|
30
|
+
} as unknown as JobsApiClient;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
client,
|
|
34
|
+
runJob,
|
|
35
|
+
getLogsUrl,
|
|
36
|
+
get lastSpec() {
|
|
37
|
+
return capturedSpec;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('uvCommand', () => {
|
|
43
|
+
it('wraps inline scripts in a shell pipeline executed via /bin/sh', async () => {
|
|
44
|
+
const harness = setupMockClient();
|
|
45
|
+
const script = 'print("hello")\nprint("world")';
|
|
46
|
+
|
|
47
|
+
await uvCommand({ script, detach: true }, harness.client);
|
|
48
|
+
|
|
49
|
+
expect(harness.runJob).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(harness.lastSpec).toBeDefined();
|
|
51
|
+
expect(harness.lastSpec?.command).toEqual([
|
|
52
|
+
'/bin/sh',
|
|
53
|
+
'-lc',
|
|
54
|
+
expect.stringContaining('uv run'),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const encoded = Buffer.from(script).toString('base64');
|
|
58
|
+
expect(harness.lastSpec?.command?.[2]).toContain(`echo "${encoded}" | base64 -d | uv run`);
|
|
59
|
+
expect(harness.lastSpec?.command?.[2]).toContain(' -');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('includes dependency and python flags when provided', async () => {
|
|
63
|
+
const harness = setupMockClient();
|
|
64
|
+
const script = 'print("deps")\nprint("python")';
|
|
65
|
+
|
|
66
|
+
await uvCommand(
|
|
67
|
+
{
|
|
68
|
+
script,
|
|
69
|
+
with_deps: ['numpy', 'pandas'],
|
|
70
|
+
python: '3.12',
|
|
71
|
+
detach: true,
|
|
72
|
+
},
|
|
73
|
+
harness.client
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const shellCommand = harness.lastSpec?.command?.[2];
|
|
77
|
+
expect(shellCommand).toContain('--with numpy');
|
|
78
|
+
expect(shellCommand).toContain('--with pandas');
|
|
79
|
+
expect(shellCommand).toContain('-p 3.12');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
declare module '@mcp-ui/server' {
|
|
2
|
-
interface HTMLTextContent {
|
|
3
|
-
uri: string;
|
|
4
|
-
mimeType: string;
|
|
5
|
-
text: string;
|
|
6
|
-
}
|
|
7
|
-
interface Base64BlobContent {
|
|
8
|
-
uri: string;
|
|
9
|
-
mimeType: string;
|
|
10
|
-
blob: string;
|
|
11
|
-
}
|
|
12
|
-
interface UIResource {
|
|
13
|
-
type: 'resource';
|
|
14
|
-
resource: HTMLTextContent | Base64BlobContent;
|
|
15
|
-
}
|
|
16
|
-
interface CreateUIResourceOptions {
|
|
17
|
-
uri: string;
|
|
18
|
-
encoding: 'text' | 'blob';
|
|
19
|
-
content: {
|
|
20
|
-
type: 'rawHtml';
|
|
21
|
-
htmlString: string;
|
|
22
|
-
} | {
|
|
23
|
-
type: 'externalUrl';
|
|
24
|
-
iframeUrl: string;
|
|
25
|
-
} | {
|
|
26
|
-
type: 'remoteDom';
|
|
27
|
-
script: string;
|
|
28
|
-
framework?: string;
|
|
29
|
-
};
|
|
30
|
-
uiMetadata?: {
|
|
31
|
-
'preferred-frame-size'?: [string, string];
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function createUIResource(options: CreateUIResourceOptions): UIResource;
|
|
35
|
-
}
|
|
36
|
-
export {};
|
|
37
|
-
//# sourceMappingURL=mcp-ui-server-shim.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-ui-server-shim.d.ts","sourceRoot":"","sources":["../../src/types/mcp-ui-server-shim.ts"],"names":[],"mappings":"AAGA,OAAO,QAAQ,gBAAgB,CAAC;IAC/B,UAAiB,eAAe;QAC/B,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACb;IAED,UAAiB,iBAAiB;QACjC,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACb;IAED,UAAiB,UAAU;QAC1B,IAAI,EAAE,UAAU,CAAC;QACjB,QAAQ,EAAE,eAAe,GAAG,iBAAiB,CAAC;KAC9C;IAED,UAAiB,uBAAuB;QACvC,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;QAC1B,OAAO,EACJ;YAAE,IAAI,EAAE,SAAS,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,GACvC;YAAE,IAAI,EAAE,aAAa,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,GAC1C;YAAE,IAAI,EAAE,WAAW,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7D,UAAU,CAAC,EAAE;YACZ,sBAAsB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;SAC1C,CAAC;KACF;IAED,SAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CAAC;CAC/E"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-ui-server-shim.js","sourceRoot":"","sources":["../../src/types/mcp-ui-server-shim.ts"],"names":[],"mappings":""}
|