docs: update all URL references to new username/slug format

Update all documentation, code, and tests to use the new endpoint formats:

- MCP: /api/mcp/{username}/{collection-slug}/{transport}
- Agent: /api/{username}/agents/{agent-uid}/conversation/{id}

Changes include:
- Update MCP URLs in collections page, docs, and test script
- Update agent API URLs in chat page, docs, tests, and cron job
- Add API key authentication requirements to all examples
- Update ARCHITECTURE.md with correct endpoint formats
- Update discord cron job to fetch owner username for new URL format
This commit is contained in:
Ajax Davis 2026-01-13 23:14:36 +10:00
parent 2b4526ba95
commit 2fb790a847
9 changed files with 163 additions and 93 deletions

View file

@ -635,10 +635,10 @@ The API is built on Next.js 16 App Router with standardized response formats.
| **Tools** | `GET /api/tools` | List/search tools | | **Tools** | `GET /api/tools` | List/search tools |
| | `POST /api/tools/execute/[...slug]` | Execute tool (SSE) | | | `POST /api/tools/execute/[...slug]` | Execute tool (SSE) |
| **Agents** | `GET /api/agents` | List user agents | | **Agents** | `GET /api/agents` | List user agents |
| | `POST /api/agents/[id]/conversation/[convId]` | Chat with agent (SSE) | | | `POST /api/{username}/agents/{uid}/conversation/{convId}` | Chat with agent (SSE) |
| **Collections** | `GET /api/collections` | List user collections | | **Collections** | `GET /api/collections` | List user collections |
| | `POST /api/collections/[id]/tools` | Add tool to collection | | | `POST /api/collections/[id]/tools` | Add tool to collection |
| **MCP** | `POST /api/mcp/{user}/{slug}/{transport}` | MCP protocol | | **MCP** | `POST /api/mcp/{username}/{slug}/{transport}` | MCP protocol |
| **Sync** | `POST /api/sync/changes` | Cron: npm changes | | **Sync** | `POST /api/sync/changes` | Cron: npm changes |
| **Stats** | `GET /api/stats` | Registry statistics | | **Stats** | `GET /api/stats` | Registry statistics |

View file

@ -1,49 +1,67 @@
#!/bin/bash #!/bin/bash
# Test script for MCP endpoints # Test script for MCP endpoints
# Usage: ./test-mcp.sh <collection-id> [base-url] # Usage: ./test-mcp.sh <username> <collection-slug> [base-url] [api-key]
#
# The new MCP URL format is: /api/mcp/{username}/{slug}/{transport}
# Collection ID is also supported as slug for backwards compatibility
COLLECTION_ID=${1:-""} USERNAME=${1:-""}
BASE_URL=${2:-"https://tpmjs.com"} COLLECTION_SLUG=${2:-""}
BASE_URL=${3:-"https://tpmjs.com"}
API_KEY=${4:-""}
if [ -z "$COLLECTION_ID" ]; then if [ -z "$USERNAME" ] || [ -z "$COLLECTION_SLUG" ]; then
echo "Usage: ./test-mcp.sh <collection-id> [base-url]" echo "Usage: ./test-mcp.sh <username> <collection-slug> [base-url] [api-key]"
echo "Example: ./test-mcp.sh clx123abc https://tpmjs.com" echo "Example: ./test-mcp.sh ajax my-collection https://tpmjs.com tpmjs_sk_xxx"
echo ""
echo "Note: Collection ID can also be used as slug for backwards compatibility"
exit 1 exit 1
fi fi
# Build auth header if API key provided
AUTH_HEADER=""
if [ -n "$API_KEY" ]; then
AUTH_HEADER="-H \"Authorization: Bearer $API_KEY\""
fi
echo "================================================" echo "================================================"
echo "Testing MCP endpoints for collection: $COLLECTION_ID" echo "Testing MCP endpoints"
echo "Username: $USERNAME"
echo "Collection: $COLLECTION_SLUG"
echo "Base URL: $BASE_URL" echo "Base URL: $BASE_URL"
echo "Auth: ${API_KEY:+API key provided}"
echo "================================================" echo "================================================"
echo "" echo ""
# Test 1: HTTP transport - GET (server info) # Test 1: HTTP transport - GET (server info)
echo "1. Testing HTTP GET (server info)..." echo "1. Testing HTTP GET (server info)..."
echo " GET $BASE_URL/api/collections/$COLLECTION_ID/mcp/http" echo " GET $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/http"
curl -s "$BASE_URL/api/collections/$COLLECTION_ID/mcp/http" | jq . eval curl -s "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/http" $AUTH_HEADER | jq .
echo "" echo ""
# Test 2: HTTP transport - initialize # Test 2: HTTP transport - initialize
echo "2. Testing HTTP POST (initialize)..." echo "2. Testing HTTP POST (initialize)..."
echo " POST $BASE_URL/api/collections/$COLLECTION_ID/mcp/http" echo " POST $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/http"
curl -s -X POST "$BASE_URL/api/collections/$COLLECTION_ID/mcp/http" \ eval curl -s -X POST "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/http" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
$AUTH_HEADER \
-d '{"jsonrpc":"2.0","method":"initialize","id":1}' | jq . -d '{"jsonrpc":"2.0","method":"initialize","id":1}' | jq .
echo "" echo ""
# Test 3: HTTP transport - tools/list # Test 3: HTTP transport - tools/list
echo "3. Testing HTTP POST (tools/list)..." echo "3. Testing HTTP POST (tools/list)..."
echo " POST $BASE_URL/api/collections/$COLLECTION_ID/mcp/http" echo " POST $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/http"
curl -s -X POST "$BASE_URL/api/collections/$COLLECTION_ID/mcp/http" \ eval curl -s -X POST "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/http" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
$AUTH_HEADER \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}' | jq . -d '{"jsonrpc":"2.0","method":"tools/list","id":2}' | jq .
echo "" echo ""
# Test 4: SSE transport - GET (event stream) # Test 4: SSE transport - GET (event stream)
echo "4. Testing SSE GET (event stream)..." echo "4. Testing SSE GET (event stream)..."
echo " GET $BASE_URL/api/collections/$COLLECTION_ID/mcp/sse" echo " GET $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/sse"
curl -s -N "$BASE_URL/api/collections/$COLLECTION_ID/mcp/sse" & eval curl -s -N "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/sse" $AUTH_HEADER &
SSE_PID=$! SSE_PID=$!
sleep 2 sleep 2
kill $SSE_PID 2>/dev/null kill $SSE_PID 2>/dev/null
@ -52,26 +70,28 @@ echo ""
# Test 5: SSE transport - initialize # Test 5: SSE transport - initialize
echo "5. Testing SSE POST (initialize)..." echo "5. Testing SSE POST (initialize)..."
echo " POST $BASE_URL/api/collections/$COLLECTION_ID/mcp/sse" echo " POST $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/sse"
curl -s -X POST "$BASE_URL/api/collections/$COLLECTION_ID/mcp/sse" \ eval curl -s -X POST "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/sse" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
$AUTH_HEADER \
-d '{"jsonrpc":"2.0","method":"initialize","id":1}' -d '{"jsonrpc":"2.0","method":"initialize","id":1}'
echo "" echo ""
echo "" echo ""
# Test 6: SSE transport - tools/list # Test 6: SSE transport - tools/list
echo "6. Testing SSE POST (tools/list)..." echo "6. Testing SSE POST (tools/list)..."
echo " POST $BASE_URL/api/collections/$COLLECTION_ID/mcp/sse" echo " POST $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/sse"
curl -s -X POST "$BASE_URL/api/collections/$COLLECTION_ID/mcp/sse" \ eval curl -s -X POST "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/sse" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
$AUTH_HEADER \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}' -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
echo "" echo ""
echo "" echo ""
# Test 7: Invalid transport # Test 7: Invalid transport
echo "7. Testing invalid transport..." echo "7. Testing invalid transport..."
echo " GET $BASE_URL/api/collections/$COLLECTION_ID/mcp/invalid" echo " GET $BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/invalid"
curl -s "$BASE_URL/api/collections/$COLLECTION_ID/mcp/invalid" | jq . eval curl -s "$BASE_URL/api/mcp/$USERNAME/$COLLECTION_SLUG/invalid" $AUTH_HEADER | jq .
echo "" echo ""
echo "================================================" echo "================================================"

View file

@ -55,6 +55,7 @@ interface Agent {
collectionCount: number; collectionCount: number;
createdBy: { createdBy: {
id: string; id: string;
username: string | null;
name: string; name: string;
image: string | null; image: string | null;
}; };
@ -266,11 +267,11 @@ export default function PublicAgentChatPage(): React.ReactElement {
// Fetch messages for conversation (initial load - gets most recent 50) // Fetch messages for conversation (initial load - gets most recent 50)
const fetchMessages = useCallback(async () => { const fetchMessages = useCallback(async () => {
if (!agent) return; if (!agent || !agent.createdBy.username) return;
try { try {
const response = await fetch( const response = await fetch(
`/api/agents/${agent.uid}/conversation/${conversationId}?limit=50` `/api/${agent.createdBy.username}/agents/${agent.uid}/conversation/${conversationId}?limit=50`
); );
if (response.status === 404) { if (response.status === 404) {
// Conversation doesn't exist yet, that's fine // Conversation doesn't exist yet, that's fine
@ -294,7 +295,14 @@ export default function PublicAgentChatPage(): React.ReactElement {
// Load older messages when scrolling up // Load older messages when scrolling up
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Virtuoso scroll handler with pagination logic // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Virtuoso scroll handler with pagination logic
const loadMoreMessages = useCallback(async () => { const loadMoreMessages = useCallback(async () => {
if (!agent || isLoadingMore || !hasMoreMessages || messages.length === 0) return; if (
!agent ||
!agent.createdBy.username ||
isLoadingMore ||
!hasMoreMessages ||
messages.length === 0
)
return;
setIsLoadingMore(true); setIsLoadingMore(true);
try { try {
@ -305,7 +313,7 @@ export default function PublicAgentChatPage(): React.ReactElement {
if (!beforeTimestamp) return; if (!beforeTimestamp) return;
const response = await fetch( const response = await fetch(
`/api/agents/${agent.uid}/conversation/${conversationId}?limit=50&before=${encodeURIComponent(beforeTimestamp)}` `/api/${agent.createdBy.username}/agents/${agent.uid}/conversation/${conversationId}?limit=50&before=${encodeURIComponent(beforeTimestamp)}`
); );
if (!response.ok) return; if (!response.ok) return;
@ -344,7 +352,7 @@ export default function PublicAgentChatPage(): React.ReactElement {
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Chat send handler with streaming and tool calls // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Chat send handler with streaming and tool calls
const handleSend = async () => { const handleSend = async () => {
if (!input.trim() || !agent || isSending) return; if (!input.trim() || !agent || !agent.createdBy.username || isSending) return;
const messageContent = input.trim(); const messageContent = input.trim();
setInput(''); setInput('');
@ -363,11 +371,14 @@ export default function PublicAgentChatPage(): React.ReactElement {
setMessages((prev) => [...prev, userMessage]); setMessages((prev) => [...prev, userMessage]);
try { try {
const response = await fetch(`/api/agents/${agent.uid}/conversation/${conversationId}`, { const response = await fetch(
method: 'POST', `/api/${agent.createdBy.username}/agents/${agent.uid}/conversation/${conversationId}`,
headers: { 'Content-Type': 'application/json' }, {
body: JSON.stringify({ message: messageContent }), method: 'POST',
}); headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: messageContent }),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();

View file

@ -47,10 +47,15 @@ export async function POST(request: NextRequest) {
} }
try { try {
// 3. Verify agent exists // 3. Verify agent exists and get owner info
const agent = await prisma.agent.findUnique({ const agent = await prisma.agent.findUnique({
where: { id: agentId }, where: { id: agentId },
select: { id: true, name: true }, select: {
id: true,
uid: true,
name: true,
user: { select: { username: true } },
},
}); });
if (!agent) { if (!agent) {
@ -60,6 +65,13 @@ export async function POST(request: NextRequest) {
); );
} }
if (!agent.user.username || !agent.uid) {
return NextResponse.json(
{ success: false, error: 'Agent owner has no username or agent has no UID' },
{ status: 500 }
);
}
// 4. Get or create a conversation for this cron job // 4. Get or create a conversation for this cron job
// Use a fixed slug for the cron job so we reuse the same conversation // Use a fixed slug for the cron job so we reuse the same conversation
const cronSlug = 'discord-summary-cron'; const cronSlug = 'discord-summary-cron';
@ -100,10 +112,10 @@ Then use the discordPostTool to post the summary to channel "${summaryChannelId}
If there are no messages in the past 24 hours, post a brief message saying the server was quiet.`; If there are no messages in the past 24 hours, post a brief message saying the server was quiet.`;
// 6. Call the agent conversation endpoint // 6. Call the agent conversation endpoint (new URL format)
const baseUrl = env.BETTER_AUTH_URL || `http://localhost:${process.env.PORT || 3000}`; const baseUrl = env.BETTER_AUTH_URL || `http://localhost:${process.env.PORT || 3000}`;
const response = await fetch( const response = await fetch(
`${baseUrl}/api/agents/${agentId}/conversation/${conversation.id}`, `${baseUrl}/api/${agent.user.username}/agents/${agent.uid}/conversation/${conversation.id}`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {

View file

@ -46,13 +46,13 @@ interface PublicCollection {
tools: CollectionTool[]; tools: CollectionTool[];
} }
function McpUrlSection({ collectionId }: { collectionId: string }) { function McpUrlSection({ username, slug }: { username: string; slug: string }) {
const [copiedUrl, setCopiedUrl] = useState<'http' | 'sse' | null>(null); const [copiedUrl, setCopiedUrl] = useState<'http' | 'sse' | null>(null);
const [showConfig, setShowConfig] = useState(false); const [showConfig, setShowConfig] = useState(false);
const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://tpmjs.com'; const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://tpmjs.com';
const httpUrl = `${baseUrl}/api/collections/${collectionId}/mcp/http`; const httpUrl = `${baseUrl}/api/mcp/${username}/${slug}/http`;
const sseUrl = `${baseUrl}/api/collections/${collectionId}/mcp/sse`; const sseUrl = `${baseUrl}/api/mcp/${username}/${slug}/sse`;
const copyToClipboard = async (url: string, type: 'http' | 'sse') => { const copyToClipboard = async (url: string, type: 'http' | 'sse') => {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
@ -66,7 +66,9 @@ function McpUrlSection({ collectionId }: { collectionId: string }) {
"command": "npx", "command": "npx",
"args": [ "args": [
"mcp-remote", "mcp-remote",
"${httpUrl}" "${httpUrl}",
"--header",
"Authorization: Bearer YOUR_TPMJS_API_KEY"
] ]
} }
} }
@ -309,7 +311,9 @@ export default function PublicCollectionDetailPage(): React.ReactElement {
</div> </div>
{/* MCP URLs */} {/* MCP URLs */}
<McpUrlSection collectionId={collection.id} /> {collection.createdBy?.username && collection.slug && (
<McpUrlSection username={collection.createdBy.username} slug={collection.slug} />
)}
{/* Tools */} {/* Tools */}
<div> <div>

View file

@ -692,18 +692,30 @@ Always cite your sources and be transparent about limitations.`}
<DocSubSection title="Endpoint"> <DocSubSection title="Endpoint">
<CodeBlock <CodeBlock
language="text" language="text"
code="POST /api/agents/[uid]/conversation/[conversationId]" code="POST /api/{username}/agents/{agent-uid}/conversation/{conversationId}"
/> />
<div className="mt-4 space-y-2 text-foreground-secondary text-sm"> <div className="mt-4 space-y-2 text-foreground-secondary text-sm">
<p> <p>
<strong className="text-foreground">uid:</strong> Your agent&apos;s unique <strong className="text-foreground">username:</strong> The agent owner&apos;s
identifier username
</p>
<p>
<strong className="text-foreground">agent-uid:</strong> The agent&apos;s unique
identifier (slug)
</p> </p>
<p> <p>
<strong className="text-foreground">conversationId:</strong> Unique ID for the <strong className="text-foreground">conversationId:</strong> Unique ID for the
conversation (create your own or use a new ID to start a new conversation) conversation (create your own or use a new ID to start a new conversation)
</p> </p>
</div> </div>
<div className="mt-4 p-4 border border-primary/30 rounded-lg bg-primary/5">
<p className="text-sm text-foreground-secondary">
<strong className="text-foreground">Authentication Required:</strong> Include
the header{' '}
<code className="text-primary">Authorization: Bearer YOUR_TPMJS_API_KEY</code>{' '}
in all requests. Get your API key from the dashboard.
</p>
</div>
</DocSubSection> </DocSubSection>
<DocSubSection title="Request Body"> <DocSubSection title="Request Body">
<ParamTable <ParamTable
@ -726,13 +738,19 @@ Always cite your sources and be transparent about limitations.`}
<DocSubSection title="Example: JavaScript Client"> <DocSubSection title="Example: JavaScript Client">
<CodeBlock <CodeBlock
language="typescript" language="typescript"
code={`const conversationId = 'conv-' + Date.now(); code={`const username = 'ajax'; // Agent owner's username
const agentUid = 'research-assistant'; // Agent UID
const conversationId = 'conv-' + Date.now();
const apiKey = 'tpmjs_sk_...'; // Your TPMJS API key
const response = await fetch( const response = await fetch(
\`https://tpmjs.com/api/agents/\${agentUid}/conversation/\${conversationId}\`, \`https://tpmjs.com/api/\${username}/agents/\${agentUid}/conversation/\${conversationId}\`,
{ {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${apiKey}\`,
},
body: JSON.stringify({ message: 'Search for AI tools' }), body: JSON.stringify({ message: 'Search for AI tools' }),
} }
); );
@ -825,11 +843,11 @@ while (true) {
POST POST
</Badge> </Badge>
<code className="text-foreground font-mono"> <code className="text-foreground font-mono">
/api/agents/[uid]/conversation/[conversationId] /api/[username]/agents/[uid]/conversation/[conversationId]
</code> </code>
</div> </div>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Send a message and stream the AI response via SSE. Send a message and stream the AI response via SSE. Requires TPMJS API key.
</p> </p>
</div> </div>
<div className="p-4 border border-border rounded-lg bg-surface"> <div className="p-4 border border-border rounded-lg bg-surface">
@ -838,11 +856,11 @@ while (true) {
GET GET
</Badge> </Badge>
<code className="text-foreground font-mono"> <code className="text-foreground font-mono">
/api/agents/[uid]/conversation/[conversationId] /api/[username]/agents/[uid]/conversation/[conversationId]
</code> </code>
</div> </div>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
Get the full conversation history with all messages. Get the full conversation history with all messages. Requires TPMJS API key.
</p> </p>
</div> </div>
<div className="p-4 border border-border rounded-lg bg-surface"> <div className="p-4 border border-border rounded-lg bg-surface">
@ -851,11 +869,11 @@ while (true) {
GET GET
</Badge> </Badge>
<code className="text-foreground font-mono"> <code className="text-foreground font-mono">
/api/agents/[uid]/conversations /api/[username]/agents/[uid]/conversations
</code> </code>
</div> </div>
<p className="text-sm text-foreground-secondary"> <p className="text-sm text-foreground-secondary">
List all conversations for an agent. List all conversations for an agent. Requires TPMJS API key.
</p> </p>
</div> </div>
<div className="p-4 border border-border rounded-lg bg-surface"> <div className="p-4 border border-border rounded-lg bg-surface">

View file

@ -1219,10 +1219,11 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
</p> </p>
<CodeBlock <CodeBlock
language="text" language="text"
code="https://tpmjs.com/api/collections/<collection-id>/mcp/http" code="https://tpmjs.com/api/mcp/<username>/<collection-slug>/http"
/> />
<p className="text-foreground-secondary mt-4"> <p className="text-foreground-secondary mt-4">
This URL is what you&apos;ll use to connect MCP clients to your collection. This URL is what you&apos;ll use to connect MCP clients to your collection.
You&apos;ll also need your TPMJS API key for authentication.
</p> </p>
</DocSubSection> </DocSubSection>
</DocSection> </DocSection>
@ -1244,7 +1245,9 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
"command": "npx", "command": "npx",
"args": [ "args": [
"mcp-remote", "mcp-remote",
"https://tpmjs.com/api/collections/<collection-id>/mcp/http" "https://tpmjs.com/api/mcp/<username>/<collection-slug>/http",
"--header",
"Authorization: Bearer YOUR_TPMJS_API_KEY"
] ]
} }
} }
@ -1275,7 +1278,8 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
<CodeBlock <CodeBlock
language="bash" language="bash"
code={`claude mcp add tpmjs-my-collection \\ code={`claude mcp add tpmjs-my-collection \\
-- npx mcp-remote https://tpmjs.com/api/collections/<collection-id>/mcp/http`} -- npx mcp-remote https://tpmjs.com/api/mcp/<username>/<collection-slug>/http \\
--header "Authorization: Bearer YOUR_TPMJS_API_KEY"`}
/> />
<p className="text-foreground-secondary mt-4"> <p className="text-foreground-secondary mt-4">
This automatically adds the server to your Claude Code configuration. This automatically adds the server to your Claude Code configuration.
@ -1305,7 +1309,7 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
<DocSubSection title="Endpoint Format"> <DocSubSection title="Endpoint Format">
<CodeBlock <CodeBlock
language="text" language="text"
code="POST https://tpmjs.com/api/collections/<collection-id>/mcp/http" code="POST https://tpmjs.com/api/mcp/<username>/<collection-slug>/http"
/> />
</DocSubSection> </DocSubSection>
<DocSubSection title="Request Headers"> <DocSubSection title="Request Headers">
@ -1318,6 +1322,9 @@ export TPMJS_EXECUTOR_URL=https://executor.mycompany.com`}
Accept: application/json, text/event-stream Accept: application/json, text/event-stream
</code> </code>
</p> </p>
<p>
<code className="text-primary">Authorization: Bearer YOUR_TPMJS_API_KEY</code>
</p>
</div> </div>
</DocSubSection> </DocSubSection>
<DocSubSection title="Supported Methods"> <DocSubSection title="Supported Methods">

View file

@ -370,7 +370,9 @@ Invalid usernames:
"command": "npx", "command": "npx",
"args": [ "args": [
"mcp-remote", "mcp-remote",
"https://tpmjs.com/api/collections/{collection-id}/mcp/http" "https://tpmjs.com/api/mcp/{username}/{collection-slug}/http",
"--header",
"Authorization: Bearer YOUR_TPMJS_API_KEY"
] ]
} }
} }
@ -602,10 +604,10 @@ Invalid usernames:
<tr> <tr>
<td className="py-3 px-4 text-foreground">Agent Conversation</td> <td className="py-3 px-4 text-foreground">Agent Conversation</td>
<td className="py-3 px-4 font-mono text-primary text-xs"> <td className="py-3 px-4 font-mono text-primary text-xs">
/api/chat/{'{username}'}/{'{uid}'}/conversation/{'{id}'} /api/{'{username}'}/agents/{'{uid}'}/conversation/{'{id}'}
</td> </td>
<td className="py-3 px-4 font-mono text-foreground-secondary text-xs"> <td className="py-3 px-4 font-mono text-foreground-secondary text-xs">
/api/chat/ajax/research-bot/conversation/abc123 /api/ajax/agents/research-bot/conversation/abc123
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -87,6 +87,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
name: string; name: string;
userId: string; userId: string;
provider: string; provider: string;
user: { username: string | null };
} | null = null; } | null = null;
let testConversationId: string | null = null; let testConversationId: string | null = null;
@ -110,11 +111,15 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
name: true, name: true,
userId: true, userId: true,
provider: true, provider: true,
user: { select: { username: true } },
}, },
}); });
if (!testAgent) { if (!testAgent) {
console.warn(`⚠️ Test agent "${TEST_AGENT_UID}" not found. Skipping agent tests.`); console.warn(`⚠️ Test agent "${TEST_AGENT_UID}" not found. Skipping agent tests.`);
} else if (!testAgent.user.username) {
console.warn(`⚠️ Test agent owner has no username. Skipping agent tests.`);
testAgent = null;
} }
} catch (error) { } catch (error) {
console.warn('⚠️ Could not connect to database. Skipping agent tests.', error); console.warn('⚠️ Could not connect to database. Skipping agent tests.', error);
@ -139,7 +144,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
} }
}); });
describe('POST /api/agents/[id]/conversation/[conversationId]', () => { describe('POST /api/[username]/agents/[uid]/conversation/[conversationId]', () => {
it('should create a new conversation and receive a response', async () => { it('should create a new conversation and receive a response', async () => {
if (!serverAvailable || !testAgent) { if (!serverAvailable || !testAgent) {
console.log('Skipping: Server not available or no test agent'); console.log('Skipping: Server not available or no test agent');
@ -147,7 +152,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
} }
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`, `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`,
{ {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -224,7 +229,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
} }
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`, `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`,
{ {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -260,7 +265,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
const toolConvSlug = `tool-test-${Date.now()}`; const toolConvSlug = `tool-test-${Date.now()}`;
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/agents/${testAgent.uid}/conversation/${toolConvSlug}`, `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversation/${toolConvSlug}`,
{ {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -312,7 +317,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
}); });
}); });
describe('GET /api/agents/[id]/conversation/[conversationId]', () => { describe('GET /api/[username]/agents/[uid]/conversation/[conversationId]', () => {
it('should retrieve conversation history', async () => { it('should retrieve conversation history', async () => {
if (!serverAvailable || !testAgent || !testConversationId) { if (!serverAvailable || !testAgent || !testConversationId) {
console.log('Skipping: Server not available or no test conversation'); console.log('Skipping: Server not available or no test conversation');
@ -320,7 +325,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
} }
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`, `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversation/${TEST_CONVERSATION_SLUG}`,
{ method: 'GET' } { method: 'GET' }
); );
@ -341,7 +346,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
} }
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/agents/${testAgent.uid}/conversation/non-existent-conv-12345`, `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversation/non-existent-conv-12345`,
{ method: 'GET' } { method: 'GET' }
); );
@ -349,16 +354,19 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
}); });
}); });
describe('GET /api/agents/[id]/conversations', () => { describe('GET /api/[username]/agents/[uid]/conversations', () => {
it('should list all conversations for an agent', async () => { it('should list all conversations for an agent', async () => {
if (!serverAvailable || !testAgent) { if (!serverAvailable || !testAgent) {
console.log('Skipping: Server not available or no test agent'); console.log('Skipping: Server not available or no test agent');
return; return;
} }
const response = await fetch(`${BASE_URL}/api/agents/${testAgent.id}/conversations`, { const response = await fetch(
method: 'GET', `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversations`,
}); {
method: 'GET',
}
);
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
@ -380,7 +388,7 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
} }
const response = await fetch( const response = await fetch(
`${BASE_URL}/api/agents/${testAgent.id}/conversations?limit=1&offset=0`, `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversations?limit=1&offset=0`,
{ method: 'GET' } { method: 'GET' }
); );
@ -433,31 +441,19 @@ describe.skipIf(!INTEGRATION_TESTS_ENABLED)('Agent Endpoints', () => {
}); });
}); });
describe('Agent with lookup by ID or UID', () => { describe('Agent with username/uid lookup', () => {
it('should accept agent ID in URL', async () => { it('should accept agent with username/uid format', async () => {
if (!serverAvailable || !testAgent) { if (!serverAvailable || !testAgent) {
console.log('Skipping: Server not available or no test agent'); console.log('Skipping: Server not available or no test agent');
return; return;
} }
const response = await fetch(`${BASE_URL}/api/agents/${testAgent.id}/conversations`, { const response = await fetch(
method: 'GET', `${BASE_URL}/api/${testAgent.user.username}/agents/${testAgent.uid}/conversations`,
}); {
method: 'GET',
expect(response.ok).toBe(true); }
const result = await response.json(); );
expect(result.success).toBe(true);
});
it('should accept agent UID in URL', async () => {
if (!serverAvailable || !testAgent) {
console.log('Skipping: Server not available or no test agent');
return;
}
const response = await fetch(`${BASE_URL}/api/agents/${testAgent.uid}/conversations`, {
method: 'GET',
});
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
const result = await response.json(); const result = await response.json();