Skip to content

Commit 85f047f

Browse files
emilioaccclaude
andauthored
Add login and tool commands (search, image, music, video, x) (#13)
* Add login and tool commands (search, image, music, video, x) - Add `login` command to save ATXP connection string to ~/.atxp/config - Add tool commands that wrap @atxp/client: - `search <query>` - web search via search.mcp.atxp.ai - `image <prompt>` - image generation via image.mcp.atxp.ai - `music <prompt>` - music generation via music.mcp.atxp.ai - `video <prompt>` - video generation via video.mcp.atxp.ai - `x <query>` - X/Twitter search via x-live-search.mcp.atxp.ai - Move demo and create commands under `dev` subcommand - Maintain backwards compatibility for `npx atxp demo` and `npx atxp create` - Update help text with new command structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add and update tests for new tool commands - Update index.test.ts with tests for dev subcommand and tool argument parsing - Add login.test.ts for config file format and login options - Add call-tool.test.ts for server mapping and result parsing - Add commands/commands.test.ts for all tool commands (search, image, music, video, x) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix lint errors - Wrap case block in braces to fix no-case-declarations error in index.ts - Remove unused fs import from login.test.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 847477e commit 85f047f

File tree

15 files changed

+12302
-2682
lines changed

15 files changed

+12302
-2682
lines changed

package-lock.json

Lines changed: 11496 additions & 2579 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/atxp/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@
3434
"create"
3535
],
3636
"dependencies": {
37+
"@atxp/client": "^0.10.5",
3738
"chalk": "^5.3.0",
38-
"inquirer": "^9.2.12",
39-
"ora": "^7.0.1",
4039
"fs-extra": "^11.2.0",
41-
"open": "^9.1.0"
40+
"inquirer": "^9.2.12",
41+
"open": "^9.1.0",
42+
"ora": "^7.0.1"
4243
},
4344
"devDependencies": {
4445
"@types/fs-extra": "^11.0.4",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('Call Tool', () => {
4+
describe('server configuration', () => {
5+
it('should map commands to correct servers', () => {
6+
const serverMap: Record<string, string> = {
7+
search: 'search.mcp.atxp.ai',
8+
image: 'image.mcp.atxp.ai',
9+
music: 'music.mcp.atxp.ai',
10+
video: 'video.mcp.atxp.ai',
11+
x: 'x-live-search.mcp.atxp.ai',
12+
};
13+
14+
expect(serverMap.search).toBe('search.mcp.atxp.ai');
15+
expect(serverMap.image).toBe('image.mcp.atxp.ai');
16+
expect(serverMap.music).toBe('music.mcp.atxp.ai');
17+
expect(serverMap.video).toBe('video.mcp.atxp.ai');
18+
expect(serverMap.x).toBe('x-live-search.mcp.atxp.ai');
19+
});
20+
21+
it('should map commands to correct tool names', () => {
22+
const toolMap: Record<string, string> = {
23+
search: 'search',
24+
image: 'generate_image',
25+
music: 'generate_music',
26+
video: 'generate_video',
27+
x: 'x_live_search',
28+
};
29+
30+
expect(toolMap.search).toBe('search');
31+
expect(toolMap.image).toBe('generate_image');
32+
expect(toolMap.music).toBe('generate_music');
33+
expect(toolMap.video).toBe('generate_video');
34+
expect(toolMap.x).toBe('x_live_search');
35+
});
36+
});
37+
38+
describe('connection string validation', () => {
39+
it('should detect missing connection string', () => {
40+
const checkConnection = (connection?: string) => {
41+
return !!connection;
42+
};
43+
44+
expect(checkConnection(undefined)).toBe(false);
45+
expect(checkConnection('')).toBe(false);
46+
expect(checkConnection('valid-connection')).toBe(true);
47+
});
48+
});
49+
50+
describe('tool result parsing', () => {
51+
it('should extract text content from result', () => {
52+
const extractText = (
53+
result: { content: Array<{ type: string; text?: string }> } | null
54+
) => {
55+
if (result?.content?.[0]?.text) {
56+
return result.content[0].text;
57+
}
58+
return null;
59+
};
60+
61+
const textResult = { content: [{ type: 'text', text: 'Hello world' }] };
62+
expect(extractText(textResult)).toBe('Hello world');
63+
64+
const emptyResult = { content: [] };
65+
expect(extractText(emptyResult)).toBe(null);
66+
67+
expect(extractText(null)).toBe(null);
68+
});
69+
70+
it('should detect binary content', () => {
71+
const isBinaryContent = (content: { data?: string; mimeType?: string }) => {
72+
return !!(content.data && content.mimeType);
73+
};
74+
75+
expect(isBinaryContent({ data: 'base64data', mimeType: 'image/png' })).toBe(true);
76+
expect(isBinaryContent({ data: 'base64data' })).toBe(false);
77+
expect(isBinaryContent({ mimeType: 'image/png' })).toBe(false);
78+
expect(isBinaryContent({})).toBe(false);
79+
});
80+
});
81+
82+
describe('server URL construction', () => {
83+
it('should construct correct server URL', () => {
84+
const constructUrl = (server: string) => `https://${server}`;
85+
86+
expect(constructUrl('search.mcp.atxp.ai')).toBe('https://search.mcp.atxp.ai');
87+
expect(constructUrl('image.mcp.atxp.ai')).toBe('https://image.mcp.atxp.ai');
88+
});
89+
});
90+
});

packages/atxp/src/call-tool.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { atxpClient, ATXPAccount } from '@atxp/client';
2+
import chalk from 'chalk';
3+
4+
export interface ToolResult {
5+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
6+
}
7+
8+
export async function callTool(
9+
server: string,
10+
tool: string,
11+
args: Record<string, unknown>
12+
): Promise<string> {
13+
const connection = process.env.ATXP_CONNECTION;
14+
15+
if (!connection) {
16+
console.error(chalk.red('Not logged in.'));
17+
console.error(`Run: ${chalk.cyan('npx atxp login')}`);
18+
process.exit(1);
19+
}
20+
21+
try {
22+
const client = await atxpClient({
23+
mcpServer: `https://${server}`,
24+
account: new ATXPAccount(connection),
25+
});
26+
27+
const result = (await client.callTool({
28+
name: tool,
29+
arguments: args,
30+
})) as ToolResult;
31+
32+
// Handle different content types
33+
if (result.content && result.content.length > 0) {
34+
const content = result.content[0];
35+
if (content.text) {
36+
return content.text;
37+
} else if (content.data && content.mimeType) {
38+
// For binary content (images, audio, video), return info about it
39+
return `[${content.mimeType} data received - ${content.data.length} bytes base64]`;
40+
}
41+
}
42+
43+
return JSON.stringify(result, null, 2);
44+
} catch (error) {
45+
const errorMessage = error instanceof Error ? error.message : String(error);
46+
console.error(chalk.red(`Error calling ${tool}: ${errorMessage}`));
47+
process.exit(1);
48+
}
49+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('Tool Commands', () => {
4+
describe('search command', () => {
5+
const SERVER = 'search.mcp.atxp.ai';
6+
const TOOL = 'search';
7+
8+
it('should have correct server', () => {
9+
expect(SERVER).toBe('search.mcp.atxp.ai');
10+
});
11+
12+
it('should have correct tool name', () => {
13+
expect(TOOL).toBe('search');
14+
});
15+
16+
it('should validate query is required', () => {
17+
const validateQuery = (query: string) => {
18+
return query && query.trim().length > 0;
19+
};
20+
21+
expect(validateQuery('test query')).toBeTruthy();
22+
expect(validateQuery('')).toBeFalsy();
23+
expect(validateQuery(' ')).toBeFalsy();
24+
});
25+
26+
it('should trim query whitespace', () => {
27+
const prepareQuery = (query: string) => query.trim();
28+
29+
expect(prepareQuery(' hello world ')).toBe('hello world');
30+
expect(prepareQuery('test')).toBe('test');
31+
});
32+
});
33+
34+
describe('image command', () => {
35+
const SERVER = 'image.mcp.atxp.ai';
36+
const TOOL = 'generate_image';
37+
38+
it('should have correct server', () => {
39+
expect(SERVER).toBe('image.mcp.atxp.ai');
40+
});
41+
42+
it('should have correct tool name', () => {
43+
expect(TOOL).toBe('generate_image');
44+
});
45+
46+
it('should validate prompt is required', () => {
47+
const validatePrompt = (prompt: string) => {
48+
return prompt && prompt.trim().length > 0;
49+
};
50+
51+
expect(validatePrompt('sunset over mountains')).toBeTruthy();
52+
expect(validatePrompt('')).toBeFalsy();
53+
});
54+
});
55+
56+
describe('music command', () => {
57+
const SERVER = 'music.mcp.atxp.ai';
58+
const TOOL = 'generate_music';
59+
60+
it('should have correct server', () => {
61+
expect(SERVER).toBe('music.mcp.atxp.ai');
62+
});
63+
64+
it('should have correct tool name', () => {
65+
expect(TOOL).toBe('generate_music');
66+
});
67+
68+
it('should validate prompt is required', () => {
69+
const validatePrompt = (prompt: string) => {
70+
return prompt && prompt.trim().length > 0;
71+
};
72+
73+
expect(validatePrompt('relaxing piano')).toBeTruthy();
74+
expect(validatePrompt('')).toBeFalsy();
75+
});
76+
});
77+
78+
describe('video command', () => {
79+
const SERVER = 'video.mcp.atxp.ai';
80+
const TOOL = 'generate_video';
81+
82+
it('should have correct server', () => {
83+
expect(SERVER).toBe('video.mcp.atxp.ai');
84+
});
85+
86+
it('should have correct tool name', () => {
87+
expect(TOOL).toBe('generate_video');
88+
});
89+
90+
it('should validate prompt is required', () => {
91+
const validatePrompt = (prompt: string) => {
92+
return prompt && prompt.trim().length > 0;
93+
};
94+
95+
expect(validatePrompt('ocean waves')).toBeTruthy();
96+
expect(validatePrompt('')).toBeFalsy();
97+
});
98+
});
99+
100+
describe('x command', () => {
101+
const SERVER = 'x-live-search.mcp.atxp.ai';
102+
const TOOL = 'x_live_search';
103+
104+
it('should have correct server', () => {
105+
expect(SERVER).toBe('x-live-search.mcp.atxp.ai');
106+
});
107+
108+
it('should have correct tool name', () => {
109+
expect(TOOL).toBe('x_live_search');
110+
});
111+
112+
it('should validate query is required', () => {
113+
const validateQuery = (query: string) => {
114+
return query && query.trim().length > 0;
115+
};
116+
117+
expect(validateQuery('trending topics')).toBeTruthy();
118+
expect(validateQuery('')).toBeFalsy();
119+
});
120+
});
121+
122+
describe('common command behavior', () => {
123+
it('should construct tool arguments correctly', () => {
124+
const buildArgs = (key: string, value: string) => {
125+
return { [key]: value.trim() };
126+
};
127+
128+
expect(buildArgs('query', 'test query')).toEqual({ query: 'test query' });
129+
expect(buildArgs('prompt', ' image prompt ')).toEqual({ prompt: 'image prompt' });
130+
});
131+
132+
it('should handle multi-word inputs', () => {
133+
const parseInput = (args: string[]) => args.join(' ');
134+
135+
expect(parseInput(['hello', 'world'])).toBe('hello world');
136+
expect(parseInput(['a', 'beautiful', 'sunset'])).toBe('a beautiful sunset');
137+
expect(parseInput([])).toBe('');
138+
});
139+
});
140+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { callTool } from '../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'image.mcp.atxp.ai';
5+
const TOOL = 'generate_image';
6+
7+
export async function imageCommand(prompt: string): Promise<void> {
8+
if (!prompt || prompt.trim().length === 0) {
9+
console.error(chalk.red('Error: Image prompt is required'));
10+
console.log(`Usage: ${chalk.cyan('npx atxp image <prompt>')}`);
11+
process.exit(1);
12+
}
13+
14+
const result = await callTool(SERVER, TOOL, { prompt: prompt.trim() });
15+
console.log(result);
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { callTool } from '../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'music.mcp.atxp.ai';
5+
const TOOL = 'generate_music';
6+
7+
export async function musicCommand(prompt: string): Promise<void> {
8+
if (!prompt || prompt.trim().length === 0) {
9+
console.error(chalk.red('Error: Music prompt is required'));
10+
console.log(`Usage: ${chalk.cyan('npx atxp music <prompt>')}`);
11+
process.exit(1);
12+
}
13+
14+
const result = await callTool(SERVER, TOOL, { prompt: prompt.trim() });
15+
console.log(result);
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { callTool } from '../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'search.mcp.atxp.ai';
5+
const TOOL = 'search';
6+
7+
export async function searchCommand(query: string): Promise<void> {
8+
if (!query || query.trim().length === 0) {
9+
console.error(chalk.red('Error: Search query is required'));
10+
console.log(`Usage: ${chalk.cyan('npx atxp search <query>')}`);
11+
process.exit(1);
12+
}
13+
14+
const result = await callTool(SERVER, TOOL, { query: query.trim() });
15+
console.log(result);
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { callTool } from '../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'video.mcp.atxp.ai';
5+
const TOOL = 'generate_video';
6+
7+
export async function videoCommand(prompt: string): Promise<void> {
8+
if (!prompt || prompt.trim().length === 0) {
9+
console.error(chalk.red('Error: Video prompt is required'));
10+
console.log(`Usage: ${chalk.cyan('npx atxp video <prompt>')}`);
11+
process.exit(1);
12+
}
13+
14+
const result = await callTool(SERVER, TOOL, { prompt: prompt.trim() });
15+
console.log(result);
16+
}

packages/atxp/src/commands/x.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { callTool } from '../call-tool.js';
2+
import chalk from 'chalk';
3+
4+
const SERVER = 'x-live-search.mcp.atxp.ai';
5+
const TOOL = 'x_live_search';
6+
7+
export async function xCommand(query: string): Promise<void> {
8+
if (!query || query.trim().length === 0) {
9+
console.error(chalk.red('Error: Search query is required'));
10+
console.log(`Usage: ${chalk.cyan('npx atxp x <query>')}`);
11+
process.exit(1);
12+
}
13+
14+
const result = await callTool(SERVER, TOOL, { query: query.trim() });
15+
console.log(result);
16+
}

0 commit comments

Comments
 (0)