feat: allow public access to agents and collections with caller credentials
Users can now access other users' PUBLIC agents and collections by providing their own credentials in the request: For agents: - Provide `providerApiKey` for LLM access - Provide `env` object with tool environment variables - Owner's stored credentials are never shared For collections (MCP): - Provide `env` in params for tool environment variables - Owner's stored credentials are never shared Returns clear errors listing missing required env vars if not provided. Files changed: - packages/types/src/agent.ts: Add providerApiKey to SendMessageSchema - apps/web/src/lib/agents/env-helpers.ts: New helper functions for env vars - apps/web/src/lib/agents/build-tools.ts: Accept callerEnvVars parameter - apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts: Allow public agent access with caller credentials - apps/web/src/lib/mcp/handlers.ts: Accept callerEnvVars, validate requirements - apps/web/src/app/api/mcp/[username]/[slug]/[transport]/route.ts: Allow public collection access, pass isOwner flag - apps/web/src/app/docs/platform-guide/page.tsx: Update access model docs
This commit is contained in:
parent
fdb61ed010
commit
7d9321d9c6
7 changed files with 478 additions and 275 deletions
|
|
@ -117,23 +117,22 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
||||||
|
|
||||||
// Fetch agent with all tool relations using agent ID
|
// Fetch agent with all tool relations using agent ID
|
||||||
const { fetchAgentWithTools, buildAgentTools } = await import('@/lib/agents/build-tools');
|
const { fetchAgentWithTools, buildAgentTools } = await import('@/lib/agents/build-tools');
|
||||||
|
const { getRequiredEnvVarsForAgent, getMissingEnvVars } = await import(
|
||||||
|
'@/lib/agents/env-helpers'
|
||||||
|
);
|
||||||
const agent = await fetchAgentWithTools(agentId);
|
const agent = await fetchAgentWithTools(agentId);
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Owner-only enforcement: Only the agent owner can chat with the agent
|
// Check ownership
|
||||||
if (authResult.userId !== agent.userId) {
|
const isOwner = authResult.userId === agent.userId;
|
||||||
return NextResponse.json(
|
|
||||||
{
|
// Non-owners can only access PUBLIC agents
|
||||||
success: false,
|
if (!isOwner && !agent.isPublic) {
|
||||||
error:
|
// Don't reveal that the agent exists - return 404
|
||||||
'Fork this agent to use it. Only the agent owner can chat with agents. ' +
|
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
||||||
'Visit the agent page to fork it to your account.',
|
|
||||||
},
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map provider to expected key name format
|
// Map provider to expected key name format
|
||||||
|
|
@ -153,29 +152,76 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's API key for this provider
|
let apiKey: string;
|
||||||
const userApiKey = await prisma.userApiKey.findUnique({
|
let callerEnvVars: Record<string, string> | undefined;
|
||||||
where: {
|
|
||||||
userId_keyName: {
|
|
||||||
userId: agent.userId,
|
|
||||||
keyName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userApiKey) {
|
if (isOwner) {
|
||||||
return NextResponse.json(
|
// Owner: use stored encrypted keys
|
||||||
{
|
const userApiKey = await prisma.userApiKey.findUnique({
|
||||||
success: false,
|
where: {
|
||||||
error: `No API key configured for ${agent.provider}. Please add your API key in settings.`,
|
userId_keyName: {
|
||||||
|
userId: agent.userId,
|
||||||
|
keyName,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
});
|
||||||
);
|
|
||||||
|
if (!userApiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `No API key configured for ${agent.provider}. Please add your API key in settings.`,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the API key
|
||||||
|
apiKey = decryptApiKey(userApiKey.encryptedKey, userApiKey.keyIv);
|
||||||
|
// callerEnvVars stays undefined - buildAgentTools will use agent's stored env vars
|
||||||
|
} else {
|
||||||
|
// Non-owner accessing public agent: must provide their own credentials
|
||||||
|
if (!parsed.data.providerApiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Public agent access requires your own API key',
|
||||||
|
details: {
|
||||||
|
code: 'MISSING_PROVIDER_KEY',
|
||||||
|
requiredProvider: agent.provider,
|
||||||
|
hint: `Provide your ${agent.provider} API key in the 'providerApiKey' field`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey = parsed.data.providerApiKey;
|
||||||
|
callerEnvVars = parsed.data.env || {};
|
||||||
|
|
||||||
|
// Check for required environment variables
|
||||||
|
const requiredEnvVars = getRequiredEnvVarsForAgent(agent);
|
||||||
|
const missingEnvVars = getMissingEnvVars(requiredEnvVars, callerEnvVars);
|
||||||
|
|
||||||
|
if (missingEnvVars.length > 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Missing required environment variables',
|
||||||
|
details: {
|
||||||
|
code: 'MISSING_ENV_VARS',
|
||||||
|
missingVars: missingEnvVars.map((e) => ({
|
||||||
|
name: e.name,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
hint: "Provide these variables in the 'env' field of your request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the API key
|
|
||||||
const apiKey = decryptApiKey(userApiKey.encryptedKey, userApiKey.keyIv);
|
|
||||||
|
|
||||||
// Get or create conversation
|
// Get or create conversation
|
||||||
let conversation = await prisma.conversation.findUnique({
|
let conversation = await prisma.conversation.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -285,7 +331,8 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
||||||
messages.push({ role: 'user', content: parsed.data.message });
|
messages.push({ role: 'user', content: parsed.data.message });
|
||||||
|
|
||||||
// Build tools from agent configuration
|
// Build tools from agent configuration
|
||||||
const tools = buildAgentTools(agent);
|
// Build tools - pass callerEnvVars if non-owner accessing public agent
|
||||||
|
const tools = buildAgentTools(agent, callerEnvVars);
|
||||||
|
|
||||||
// Get the provider model
|
// Get the provider model
|
||||||
const model = await getProviderModel(agent.provider, agent.modelId, apiKey);
|
const model = await getProviderModel(agent.provider, agent.modelId, apiKey);
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,8 @@ interface JsonRpcResponse {
|
||||||
async function processJsonRpcRequest(
|
async function processJsonRpcRequest(
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
body: JsonRpcRequest
|
body: JsonRpcRequest,
|
||||||
|
isOwner: boolean
|
||||||
): Promise<JsonRpcResponse> {
|
): Promise<JsonRpcResponse> {
|
||||||
const requestId = body.id ?? null;
|
const requestId = body.id ?? null;
|
||||||
|
|
||||||
|
|
@ -98,12 +99,19 @@ async function processJsonRpcRequest(
|
||||||
case 'tools/list':
|
case 'tools/list':
|
||||||
return await handleToolsList(collectionId, requestId);
|
return await handleToolsList(collectionId, requestId);
|
||||||
|
|
||||||
case 'tools/call':
|
case 'tools/call': {
|
||||||
return await handleToolsCall(
|
const params = body.params as {
|
||||||
collectionId,
|
name: string;
|
||||||
body.params as { name: string; arguments?: Record<string, unknown> },
|
arguments?: Record<string, unknown>;
|
||||||
requestId
|
env?: Record<string, string>;
|
||||||
);
|
};
|
||||||
|
|
||||||
|
// For non-owners, use caller-provided env vars (or empty if not provided)
|
||||||
|
// For owners, callerEnvVars is undefined so handleToolsCall uses stored env vars
|
||||||
|
const callerEnvVars = isOwner ? undefined : params.env || {};
|
||||||
|
|
||||||
|
return await handleToolsCall(collectionId, params, requestId, callerEnvVars);
|
||||||
|
}
|
||||||
|
|
||||||
case 'notifications/initialized':
|
case 'notifications/initialized':
|
||||||
case 'ping':
|
case 'ping':
|
||||||
|
|
@ -125,7 +133,8 @@ async function processJsonRpcRequest(
|
||||||
async function handleHttpTransport(
|
async function handleHttpTransport(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
collectionName: string
|
collectionName: string,
|
||||||
|
isOwner: boolean
|
||||||
): Promise<NextResponse> {
|
): Promise<NextResponse> {
|
||||||
let body: JsonRpcRequest;
|
let body: JsonRpcRequest;
|
||||||
try {
|
try {
|
||||||
|
|
@ -137,7 +146,7 @@ async function handleHttpTransport(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await processJsonRpcRequest(collectionId, collectionName, body);
|
const response = await processJsonRpcRequest(collectionId, collectionName, body, isOwner);
|
||||||
return NextResponse.json(response);
|
return NextResponse.json(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +157,8 @@ async function handleHttpTransport(
|
||||||
async function handleSseTransport(
|
async function handleSseTransport(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
collectionName: string
|
collectionName: string,
|
||||||
|
isOwner: boolean
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
let body: JsonRpcRequest;
|
let body: JsonRpcRequest;
|
||||||
try {
|
try {
|
||||||
|
|
@ -167,7 +177,7 @@ async function handleSseTransport(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await processJsonRpcRequest(collectionId, collectionName, body);
|
const response = await processJsonRpcRequest(collectionId, collectionName, body, isOwner);
|
||||||
|
|
||||||
// For SSE, we send the response as an event and then close
|
// For SSE, we send the response as an event and then close
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
@ -331,7 +341,7 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
||||||
|
|
||||||
// Authorization check:
|
// Authorization check:
|
||||||
// - Owners can always access their own collections (public or private)
|
// - Owners can always access their own collections (public or private)
|
||||||
// - Non-owners can only access public collections (and must fork to use)
|
// - Non-owners can access PUBLIC collections with their own env vars
|
||||||
const isOwner = authResult.userId === collection.userId;
|
const isOwner = authResult.userId === collection.userId;
|
||||||
|
|
||||||
if (!isOwner) {
|
if (!isOwner) {
|
||||||
|
|
@ -342,27 +352,15 @@ export async function POST(request: NextRequest, context: RouteContext): Promise
|
||||||
{ status: 404 }
|
{ status: 404 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Public collection but not the owner - they need to fork it
|
// Public collection - non-owners can access but must provide their own env vars
|
||||||
return NextResponse.json(
|
// The env vars are validated per-tool in handleToolsCall
|
||||||
{
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
error: {
|
|
||||||
code: -32403,
|
|
||||||
message:
|
|
||||||
'Fork this collection to use it. Only the collection owner can execute tools via MCP. ' +
|
|
||||||
'Visit the collection page to fork it to your account.',
|
|
||||||
},
|
|
||||||
id: null,
|
|
||||||
},
|
|
||||||
{ status: 403 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
if (transport === 'sse') {
|
if (transport === 'sse') {
|
||||||
response = await handleSseTransport(request, collection.id, collection.name);
|
response = await handleSseTransport(request, collection.id, collection.name, isOwner);
|
||||||
} else {
|
} else {
|
||||||
response = await handleHttpTransport(request, collection.id, collection.name);
|
response = await handleHttpTransport(request, collection.id, collection.name, isOwner);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track usage for authenticated requests
|
// Track usage for authenticated requests
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,15 @@ const NAV_SECTIONS = [
|
||||||
{ id: 'api-usage', label: 'Usage Tracking' },
|
{ id: 'api-usage', label: 'Usage Tracking' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Access Model',
|
||||||
|
items: [
|
||||||
|
{ id: 'access-model', label: 'Overview' },
|
||||||
|
{ id: 'agent-access', label: 'Agent API Access' },
|
||||||
|
{ id: 'collection-access', label: 'Collection MCP Access' },
|
||||||
|
{ id: 'error-reference', label: 'Error Reference' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Reference',
|
title: 'Reference',
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -1633,36 +1642,48 @@ X-RateLimit-Reset: 1704067200`}
|
||||||
</DocSection>
|
</DocSection>
|
||||||
|
|
||||||
{/* ==================== ACCESS MODEL ==================== */}
|
{/* ==================== ACCESS MODEL ==================== */}
|
||||||
<DocSection id="access-model" title="Access Model: Fork to Use">
|
<DocSection id="access-model" title="Access Model: Public Access with Your Credentials">
|
||||||
<p className="text-foreground-secondary mb-6">
|
<p className="text-foreground-secondary mb-6">
|
||||||
TPMJS uses a <strong className="text-foreground">"fork to use"</strong>{' '}
|
You can access public agents and collections using your own API key—but you must
|
||||||
model. You cannot directly use someone else's public agent or collection with
|
provide <strong className="text-foreground">all required credentials</strong>{' '}
|
||||||
your API key—you must fork it first.
|
(LLM keys, tool environment variables) in the request. The owner's stored
|
||||||
|
credentials are never shared.
|
||||||
</p>
|
</p>
|
||||||
<div className="p-4 border border-warning/30 rounded-lg bg-warning/5 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
<p className="text-sm text-foreground-secondary">
|
<div className="p-4 border border-border rounded-lg bg-surface">
|
||||||
<strong className="text-warning">Important:</strong> Even with a valid API key,
|
<h4 className="font-semibold text-foreground mb-2">Owners</h4>
|
||||||
you cannot call another user's agent or collection endpoints. You'll
|
<ul className="text-sm text-foreground-secondary space-y-1">
|
||||||
receive a 403 error instructing you to fork the resource first.
|
<li>• Use stored credentials from dashboard</li>
|
||||||
</p>
|
<li>• Access public and private resources</li>
|
||||||
|
<li>• No extra parameters needed in API calls</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 border border-border rounded-lg bg-surface">
|
||||||
|
<h4 className="font-semibold text-foreground mb-2">Non-Owners</h4>
|
||||||
|
<ul className="text-sm text-foreground-secondary space-y-1">
|
||||||
|
<li>• Can only access PUBLIC resources</li>
|
||||||
|
<li>• Must provide credentials in each request</li>
|
||||||
|
<li>• Owner's credentials are never used</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DocSubSection title="Why Fork to Use?">
|
<DocSubSection title="Why This Model?">
|
||||||
<ul className="list-disc list-inside space-y-2 text-foreground-secondary">
|
<ul className="list-disc list-inside space-y-2 text-foreground-secondary">
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Security</strong> - Owners control their own
|
<strong className="text-foreground">Security</strong> - Owner credentials stay
|
||||||
API keys and environment variables
|
private, never shared with callers
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Cost Control</strong> - You pay for your own
|
<strong className="text-foreground">Transparency</strong> - Callers pay for
|
||||||
usage, not someone else's
|
their own LLM/tool usage
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Customization</strong> - Forking lets you
|
<strong className="text-foreground">Flexibility</strong> - Use public resources
|
||||||
modify tools, prompts, and settings
|
without forking
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-foreground">Privacy</strong> - Your conversations and
|
<strong className="text-foreground">Control</strong> - Fork when you want to
|
||||||
usage stay in your account
|
customize
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
|
|
@ -1670,239 +1691,164 @@ X-RateLimit-Reset: 1704067200`}
|
||||||
|
|
||||||
<DocSection id="agent-access" title="Accessing Agents via API">
|
<DocSection id="agent-access" title="Accessing Agents via API">
|
||||||
<p className="text-foreground-secondary mb-6">
|
<p className="text-foreground-secondary mb-6">
|
||||||
When you call the agent conversation API, strict ownership checks apply.
|
Public agents can be accessed by any authenticated user who provides their own
|
||||||
|
credentials.
|
||||||
</p>
|
</p>
|
||||||
<DocSubSection title="Can I Chat with Someone Else's Public Agent?">
|
<DocSubSection title="Accessing Your Own Agent">
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
<strong className="text-foreground">No.</strong> Even if an agent is public, you
|
As the owner, just send your message—your stored LLM key and env vars are used:
|
||||||
must be the owner to use the conversation API. Attempting to call another
|
|
||||||
user's agent returns:
|
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
language="json"
|
language="json"
|
||||||
code={`{
|
code={`POST /api/agents/{agentId}/conversation/{conversationId}
|
||||||
"success": false,
|
Authorization: Bearer YOUR_TPMJS_API_KEY
|
||||||
"error": "Fork this agent to use it. Only the agent owner can chat with agents. Visit the agent page to fork it to your account."
|
|
||||||
}
|
{
|
||||||
// HTTP 403 Forbidden`}
|
"message": "Hello, help me with..."
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="Who Pays for Agent LLM Calls?">
|
<DocSubSection title="Accessing Someone Else's Public Agent">
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
The <strong className="text-foreground">agent owner</strong> always pays. The
|
You must provide your own LLM provider key and any tool environment variables:
|
||||||
system uses the owner's stored LLM provider API keys (OpenAI, Anthropic,
|
|
||||||
etc.), not the caller's.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="p-4 border border-border rounded-lg bg-surface">
|
<CodeBlock
|
||||||
<p className="text-sm text-foreground-secondary">
|
language="json"
|
||||||
Since you must own the agent to use it, and the owner's API keys are
|
code={`POST /api/agents/{agentId}/conversation/{conversationId}
|
||||||
used—you're always paying for your own usage.
|
Authorization: Bearer YOUR_TPMJS_API_KEY
|
||||||
</p>
|
|
||||||
</div>
|
{
|
||||||
|
"message": "Hello, help me with...",
|
||||||
|
"providerApiKey": "sk-your-openai-key...",
|
||||||
|
"env": {
|
||||||
|
"UNSANDBOX_API_KEY": "your-unsandbox-key",
|
||||||
|
"FIRECRAWL_API_KEY": "your-firecrawl-key"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="Missing Provider API Keys">
|
<DocSubSection title="Missing Provider API Key">
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
If you haven't configured your LLM provider API key, you'll receive:
|
If you don't provide <code>providerApiKey</code> for a public agent:
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
language="json"
|
language="json"
|
||||||
code={`{
|
code={`{
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": "No API key configured for OPENAI. Please add your API key in settings."
|
"error": "Public agent access requires your own API key",
|
||||||
|
"details": {
|
||||||
|
"code": "MISSING_PROVIDER_KEY",
|
||||||
|
"requiredProvider": "OPENAI",
|
||||||
|
"hint": "Provide your OPENAI API key in the 'providerApiKey' field"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// HTTP 400 Bad Request`}
|
// HTTP 400 Bad Request`}
|
||||||
/>
|
/>
|
||||||
<p className="text-foreground-secondary mt-4">
|
|
||||||
Add your provider keys at{' '}
|
|
||||||
<Link
|
|
||||||
href="/dashboard/settings/api-keys"
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
Dashboard → Settings → API Keys
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="Agent API Workflow">
|
<DocSubSection title="Missing Environment Variables">
|
||||||
<div className="space-y-3">
|
<p className="text-foreground-secondary mb-4">
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
If the agent's tools require env vars you didn't provide:
|
||||||
<div className="flex items-center gap-2 mb-1">
|
</p>
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
<CodeBlock
|
||||||
1
|
language="json"
|
||||||
</span>
|
code={`{
|
||||||
<span className="font-medium text-foreground">Find a public agent</span>
|
"success": false,
|
||||||
</div>
|
"error": "Missing required environment variables",
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
"details": {
|
||||||
Browse at tpmjs.com/{'{username}'}/agents/{'{uid}'}
|
"code": "MISSING_ENV_VARS",
|
||||||
</p>
|
"missingVars": [
|
||||||
</div>
|
{ "name": "UNSANDBOX_API_KEY", "description": "API key for code execution" },
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
{ "name": "FIRECRAWL_API_KEY", "description": "API key for web scraping" }
|
||||||
<div className="flex items-center gap-2 mb-1">
|
],
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
"hint": "Provide these variables in the 'env' field of your request"
|
||||||
2
|
}
|
||||||
</span>
|
}
|
||||||
<span className="font-medium text-foreground">Fork it to your account</span>
|
// HTTP 400 Bad Request`}
|
||||||
</div>
|
/>
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
</DocSubSection>
|
||||||
Click "Fork" to create your own copy
|
<DocSubSection title="Who Pays?">
|
||||||
</p>
|
<div className="p-4 border border-border rounded-lg bg-surface">
|
||||||
</div>
|
<p className="text-foreground-secondary">
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
<strong className="text-foreground">The caller always pays.</strong> When
|
||||||
<div className="flex items-center gap-2 mb-1">
|
accessing a public agent, you provide your own LLM API key—so LLM costs go to
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
your account. When accessing your own agent, your stored keys are used.
|
||||||
3
|
</p>
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground">Add your LLM API key</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
|
||||||
Configure your provider key (not copied during fork)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
|
||||||
4
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
Use your fork via API
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
|
||||||
Call /api/{'{your-username}'}/agents/{'{uid}'}/conversation/{'{id}'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
</DocSection>
|
</DocSection>
|
||||||
|
|
||||||
<DocSection id="collection-access" title="Accessing Collections via MCP">
|
<DocSection id="collection-access" title="Accessing Collections via MCP">
|
||||||
<p className="text-foreground-secondary mb-6">
|
<p className="text-foreground-secondary mb-6">
|
||||||
MCP endpoints for collections follow the same fork-to-use model.
|
Public collections can be accessed by any authenticated user who provides their own
|
||||||
|
environment variables.
|
||||||
</p>
|
</p>
|
||||||
<DocSubSection title="Can I Use Someone Else's Public Collection?">
|
<DocSubSection title="Accessing Your Own Collection">
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
<strong className="text-foreground">No.</strong> Even if a collection is public,
|
As the owner, just make MCP calls—your stored env vars are used:
|
||||||
you must be the owner to execute tools via MCP. Attempting to call another
|
|
||||||
user's collection returns:
|
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
language="json"
|
language="json"
|
||||||
code={`{
|
code={`{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"method": "tools/call",
|
||||||
"code": -32403,
|
"params": {
|
||||||
"message": "Fork this collection to use it. Only the collection owner can execute tools via MCP. Visit the collection page to fork it to your account."
|
"name": "tpmjs-unsandbox-executeCode",
|
||||||
}
|
"arguments": { "code": "print('hello')" }
|
||||||
}
|
},
|
||||||
// HTTP 403 Forbidden`}
|
"id": 1
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="Who Pays for Tool Execution?">
|
<DocSubSection title="Accessing Someone Else's Public Collection">
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
The <strong className="text-foreground">caller</strong> (API key owner) pays for
|
You must provide environment variables in the <code>env</code> field of{' '}
|
||||||
tool execution. Usage is tracked against your account and counts against your rate
|
<code>params</code>:
|
||||||
limits.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<CodeBlock
|
||||||
<div className="p-4 border border-border rounded-lg bg-surface">
|
language="json"
|
||||||
<h4 className="font-semibold text-foreground mb-2">You Provide</h4>
|
code={`{
|
||||||
<ul className="text-sm text-foreground-secondary space-y-1">
|
"jsonrpc": "2.0",
|
||||||
<li>• Your TPMJS API key (for auth)</li>
|
"method": "tools/call",
|
||||||
<li>• Your rate limit quota</li>
|
"params": {
|
||||||
</ul>
|
"name": "tpmjs-unsandbox-executeCode",
|
||||||
</div>
|
"arguments": { "code": "print('hello')" },
|
||||||
<div className="p-4 border border-border rounded-lg bg-surface">
|
"env": {
|
||||||
<h4 className="font-semibold text-foreground mb-2">Collection Provides</h4>
|
"UNSANDBOX_API_KEY": "your-unsandbox-key"
|
||||||
<ul className="text-sm text-foreground-secondary space-y-1">
|
}
|
||||||
<li>• Tool environment variables</li>
|
},
|
||||||
<li>• Executor configuration</li>
|
"id": 1
|
||||||
</ul>
|
}`}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
</DocSubSection>
|
|
||||||
<DocSubSection title="Environment Variables">
|
|
||||||
<p className="text-foreground-secondary mb-4">
|
|
||||||
Tools use the <strong className="text-foreground">collection owner's</strong>{' '}
|
|
||||||
stored environment variables—not yours. You cannot pass custom env vars via the
|
|
||||||
MCP request.
|
|
||||||
</p>
|
|
||||||
<div className="p-4 border border-border rounded-lg bg-surface">
|
|
||||||
<p className="text-sm text-foreground-secondary">
|
|
||||||
<strong className="text-foreground">Example:</strong> If you fork a web scraping
|
|
||||||
collection, you must add your own FIRECRAWL_API_KEY in the collection settings.
|
|
||||||
The original owner's key is not copied.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="Missing Environment Variables">
|
<DocSubSection title="Missing Environment Variables">
|
||||||
<p className="text-foreground-secondary mb-4">
|
<p className="text-foreground-secondary mb-4">
|
||||||
If tools require environment variables that aren't configured, the behavior
|
If you don't provide required env vars:
|
||||||
depends on the tool. Most tools will return an error in the result:
|
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
language="json"
|
language="json"
|
||||||
code={`{
|
code={`{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"result": {
|
"error": {
|
||||||
"content": [{ "type": "text", "text": "Error: FIRECRAWL_API_KEY is required" }],
|
"code": -32602,
|
||||||
"isError": true
|
"message": "Missing required environment variables",
|
||||||
|
"data": {
|
||||||
|
"missingVars": [
|
||||||
|
{ "name": "UNSANDBOX_API_KEY", "description": "API key for code execution" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<p className="text-foreground-secondary mt-4">
|
|
||||||
Check each tool's required environment variables and add them in your
|
|
||||||
collection's Env Vars tab.
|
|
||||||
</p>
|
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
<DocSubSection title="Collection MCP Workflow">
|
<DocSubSection title="Who Pays?">
|
||||||
<div className="space-y-3">
|
<div className="p-4 border border-border rounded-lg bg-surface">
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
<p className="text-foreground-secondary">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<strong className="text-foreground">The caller always pays.</strong> When
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
accessing a public collection, you provide your own tool API keys—so tool costs
|
||||||
1
|
(if any) go to your accounts. Rate limits are tracked against your TPMJS API
|
||||||
</span>
|
key.
|
||||||
<span className="font-medium text-foreground">Find a public collection</span>
|
</p>
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
|
||||||
Browse at tpmjs.com/{'{username}'}/collections/{'{slug}'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
|
||||||
2
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground">Fork it to your account</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
|
||||||
Click "Fork" to create your own copy
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
|
||||||
3
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground">Add environment variables</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
|
||||||
Configure tool API keys (not copied during fork)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 border border-border rounded-lg bg-surface">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="w-6 h-6 rounded-full bg-primary/10 text-primary font-bold flex items-center justify-center text-xs">
|
|
||||||
4
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
Use your fork via MCP
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground-secondary ml-8">
|
|
||||||
Connect to /api/mcp/{'{your-username}'}/{'{slug}'}/http
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DocSubSection>
|
</DocSubSection>
|
||||||
</DocSection>
|
</DocSection>
|
||||||
|
|
@ -1940,13 +1886,13 @@ X-RateLimit-Reset: 1704067200`}
|
||||||
<div className="p-4 border border-border rounded-lg bg-surface">
|
<div className="p-4 border border-border rounded-lg bg-surface">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Badge variant="error" size="sm">
|
<Badge variant="error" size="sm">
|
||||||
403
|
404
|
||||||
</Badge>
|
</Badge>
|
||||||
<code className="text-foreground font-mono text-sm">Fork Required</code>
|
<code className="text-foreground font-mono text-sm">Not Found / Private</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-foreground-secondary">
|
<p className="text-sm text-foreground-secondary">
|
||||||
You're trying to use someone else's agent/collection. Fork it first,
|
Resource doesn't exist or is private. Non-owners can only access PUBLIC
|
||||||
then use your own copy.
|
agents/collections.
|
||||||
</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">
|
||||||
|
|
@ -1957,8 +1903,20 @@ X-RateLimit-Reset: 1704067200`}
|
||||||
<code className="text-foreground font-mono text-sm">Missing Provider Key</code>
|
<code className="text-foreground font-mono text-sm">Missing Provider Key</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-foreground-secondary">
|
<p className="text-sm text-foreground-secondary">
|
||||||
Agent's LLM provider key not configured. Add it at Dashboard → Settings →
|
For public agents: provide <code>providerApiKey</code> in your request. For your
|
||||||
API Keys.
|
own agents: add your LLM key in Dashboard → Settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 border border-border rounded-lg bg-surface">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge variant="warning" size="sm">
|
||||||
|
400
|
||||||
|
</Badge>
|
||||||
|
<code className="text-foreground font-mono text-sm">Missing Env Vars</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-foreground-secondary">
|
||||||
|
Tools require environment variables you didn't provide. Check the error
|
||||||
|
details for the list of required vars.
|
||||||
</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">
|
||||||
|
|
|
||||||
|
|
@ -237,9 +237,14 @@ function mergeEnvVars(
|
||||||
* - Collection env vars are used as base
|
* - Collection env vars are used as base
|
||||||
* - Agent env vars override collection env vars for same keys
|
* - Agent env vars override collection env vars for same keys
|
||||||
* - Both are merged together for unique keys
|
* - Both are merged together for unique keys
|
||||||
|
*
|
||||||
|
* @param agent - The agent with tool relations
|
||||||
|
* @param callerEnvVars - Optional env vars provided by the caller (for non-owners accessing public agents).
|
||||||
|
* When provided, these are used INSTEAD of the agent's stored env vars.
|
||||||
*/
|
*/
|
||||||
export function buildAgentTools(
|
export function buildAgentTools(
|
||||||
agent: AgentWithRelations
|
agent: AgentWithRelations,
|
||||||
|
callerEnvVars?: Record<string, string>
|
||||||
): Record<string, ReturnType<typeof createToolDefinition>> {
|
): Record<string, ReturnType<typeof createToolDefinition>> {
|
||||||
const tools: Record<string, ReturnType<typeof createToolDefinition>> = {};
|
const tools: Record<string, ReturnType<typeof createToolDefinition>> = {};
|
||||||
const seenTools = new Set<string>();
|
const seenTools = new Set<string>();
|
||||||
|
|
@ -247,8 +252,10 @@ export function buildAgentTools(
|
||||||
// Parse agent-level executor config
|
// Parse agent-level executor config
|
||||||
const agentExecutorConfig = parseExecutorConfig(agent.executorType, agent.executorConfig);
|
const agentExecutorConfig = parseExecutorConfig(agent.executorType, agent.executorConfig);
|
||||||
|
|
||||||
// Parse agent-level env vars
|
// When callerEnvVars is provided (non-owner using public agent), use those exclusively.
|
||||||
const agentEnvVars = parseEnvVars(agent.envVars);
|
// Otherwise, use the agent's stored env vars.
|
||||||
|
const useCallerEnvVars = callerEnvVars !== undefined;
|
||||||
|
const agentEnvVars = useCallerEnvVars ? callerEnvVars : parseEnvVars(agent.envVars);
|
||||||
|
|
||||||
// Add tools from collections first
|
// Add tools from collections first
|
||||||
for (const agentCollection of agent.collections) {
|
for (const agentCollection of agent.collections) {
|
||||||
|
|
@ -264,7 +271,8 @@ export function buildAgentTools(
|
||||||
const resolvedConfig = resolveExecutorConfig(agentExecutorConfig, collectionExecutorConfig);
|
const resolvedConfig = resolveExecutorConfig(agentExecutorConfig, collectionExecutorConfig);
|
||||||
|
|
||||||
// Parse collection-level env vars and merge with agent env vars
|
// Parse collection-level env vars and merge with agent env vars
|
||||||
const collectionEnvVars = parseEnvVars(collection.envVars);
|
// When callerEnvVars is provided, skip collection env vars entirely
|
||||||
|
const collectionEnvVars = useCallerEnvVars ? {} : parseEnvVars(collection.envVars);
|
||||||
const mergedEnvVars = mergeEnvVars(collectionEnvVars, agentEnvVars);
|
const mergedEnvVars = mergeEnvVars(collectionEnvVars, agentEnvVars);
|
||||||
|
|
||||||
for (const collectionTool of collection.tools) {
|
for (const collectionTool of collection.tools) {
|
||||||
|
|
|
||||||
156
apps/web/src/lib/agents/env-helpers.ts
Normal file
156
apps/web/src/lib/agents/env-helpers.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* Helper functions for working with environment variables in agents and collections
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Agent, AgentCollection, AgentTool, Collection, Package, Tool } from '@tpmjs/db';
|
||||||
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import type { TpmjsEnv } from '@tpmjs/types/tpmjs';
|
||||||
|
|
||||||
|
// Reuse the AgentWithRelations type
|
||||||
|
type AgentWithRelations = Agent & {
|
||||||
|
collections: (AgentCollection & {
|
||||||
|
collection: Collection & {
|
||||||
|
tools: Array<{
|
||||||
|
tool: Tool & { package: Package };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
})[];
|
||||||
|
tools: (AgentTool & {
|
||||||
|
tool: Tool & { package: Package };
|
||||||
|
})[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all required environment variables for an agent's tools
|
||||||
|
* Collects env vars from all tools in collections and individual tools
|
||||||
|
*/
|
||||||
|
export function getRequiredEnvVarsForAgent(agent: AgentWithRelations): TpmjsEnv[] {
|
||||||
|
const envVars = new Map<string, TpmjsEnv>();
|
||||||
|
|
||||||
|
// Collect from collections
|
||||||
|
for (const agentCollection of agent.collections) {
|
||||||
|
for (const collectionTool of agentCollection.collection.tools) {
|
||||||
|
const packageEnv = collectionTool.tool.package.env as TpmjsEnv[] | null;
|
||||||
|
if (packageEnv && Array.isArray(packageEnv)) {
|
||||||
|
for (const env of packageEnv) {
|
||||||
|
if (!envVars.has(env.name)) {
|
||||||
|
envVars.set(env.name, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect from individual tools
|
||||||
|
for (const agentTool of agent.tools) {
|
||||||
|
const packageEnv = agentTool.tool.package.env as TpmjsEnv[] | null;
|
||||||
|
if (packageEnv && Array.isArray(packageEnv)) {
|
||||||
|
for (const env of packageEnv) {
|
||||||
|
if (!envVars.has(env.name)) {
|
||||||
|
envVars.set(env.name, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(envVars.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get required environment variables for a specific tool in a collection
|
||||||
|
*/
|
||||||
|
export async function getRequiredEnvVarsForCollectionTool(
|
||||||
|
collectionId: string,
|
||||||
|
toolName: string
|
||||||
|
): Promise<TpmjsEnv[]> {
|
||||||
|
const collection = await prisma.collection.findUnique({
|
||||||
|
where: { id: collectionId },
|
||||||
|
include: {
|
||||||
|
tools: {
|
||||||
|
include: {
|
||||||
|
tool: {
|
||||||
|
include: { package: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collection) return [];
|
||||||
|
|
||||||
|
// Parse tool name to find the matching tool
|
||||||
|
// Tool names are in format: sanitized-package-name-toolName
|
||||||
|
// e.g., "tpmjs-unsandbox-executeCode"
|
||||||
|
for (const ct of collection.tools) {
|
||||||
|
const pkgName = ct.tool.package.npmPackageName;
|
||||||
|
// Sanitize the package name the same way as in tool-converter
|
||||||
|
const sanitizedPkg = pkgName.replace(/[@/]/g, '-').replace(/^-+/, '');
|
||||||
|
const expectedToolName = `${sanitizedPkg}-${ct.tool.name}`;
|
||||||
|
|
||||||
|
if (toolName === expectedToolName || toolName.includes(sanitizedPkg)) {
|
||||||
|
const packageEnv = ct.tool.package.env as TpmjsEnv[] | null;
|
||||||
|
return packageEnv || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all required environment variables for all tools in a collection
|
||||||
|
*/
|
||||||
|
export async function getRequiredEnvVarsForCollection(collectionId: string): Promise<TpmjsEnv[]> {
|
||||||
|
const collection = await prisma.collection.findUnique({
|
||||||
|
where: { id: collectionId },
|
||||||
|
include: {
|
||||||
|
tools: {
|
||||||
|
include: {
|
||||||
|
tool: {
|
||||||
|
include: { package: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collection) return [];
|
||||||
|
|
||||||
|
const envVars = new Map<string, TpmjsEnv>();
|
||||||
|
|
||||||
|
for (const ct of collection.tools) {
|
||||||
|
const packageEnv = ct.tool.package.env as TpmjsEnv[] | null;
|
||||||
|
if (packageEnv && Array.isArray(packageEnv)) {
|
||||||
|
for (const env of packageEnv) {
|
||||||
|
if (!envVars.has(env.name)) {
|
||||||
|
envVars.set(env.name, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(envVars.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check which required env vars are missing from provided env vars
|
||||||
|
* Returns only the required vars that don't have defaults and aren't provided
|
||||||
|
*/
|
||||||
|
export function getMissingEnvVars(
|
||||||
|
requiredEnvVars: TpmjsEnv[],
|
||||||
|
providedEnvVars: Record<string, string>
|
||||||
|
): TpmjsEnv[] {
|
||||||
|
return requiredEnvVars.filter(
|
||||||
|
(env) => env.required !== false && !env.default && !providedEnvVars[env.name]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that provided env var names are in the allowed list
|
||||||
|
* Returns invalid names that shouldn't be accepted
|
||||||
|
*/
|
||||||
|
export function getInvalidEnvVarNames(
|
||||||
|
providedEnvVars: Record<string, string>,
|
||||||
|
allowedEnvNames: string[]
|
||||||
|
): string[] {
|
||||||
|
return Object.keys(providedEnvVars).filter((name) => !allowedEnvNames.includes(name));
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
|
import type { TpmjsEnv } from '@tpmjs/types/tpmjs';
|
||||||
import { queueBridgeToolCall, waitForBridgeResult } from '~/app/api/bridge/route';
|
import { queueBridgeToolCall, waitForBridgeResult } from '~/app/api/bridge/route';
|
||||||
import { executeWithExecutor, parseExecutorConfig } from '../executors';
|
import { executeWithExecutor, parseExecutorConfig } from '../executors';
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,7 +17,7 @@ interface JsonRpcResponse {
|
||||||
jsonrpc: '2.0';
|
jsonrpc: '2.0';
|
||||||
id: JsonRpcId;
|
id: JsonRpcId;
|
||||||
result?: unknown;
|
result?: unknown;
|
||||||
error?: { code: number; message: string };
|
error?: { code: number; message: string; data?: unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,15 +121,19 @@ export async function handleToolsList(
|
||||||
interface ToolsCallParams {
|
interface ToolsCallParams {
|
||||||
name: string;
|
name: string;
|
||||||
arguments?: Record<string, unknown>;
|
arguments?: Record<string, unknown>;
|
||||||
|
env?: Record<string, string>; // Caller-provided env vars for non-owners
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle MCP tools/call request
|
* Handle MCP tools/call request
|
||||||
|
* @param callerEnvVars - Optional env vars from caller (non-owner accessing public collection)
|
||||||
|
* When provided, these are used INSTEAD of collection's stored env vars
|
||||||
*/
|
*/
|
||||||
export async function handleToolsCall(
|
export async function handleToolsCall(
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
params: ToolsCallParams,
|
params: ToolsCallParams,
|
||||||
requestId: JsonRpcId
|
requestId: JsonRpcId,
|
||||||
|
callerEnvVars?: Record<string, string>
|
||||||
): Promise<JsonRpcResponse> {
|
): Promise<JsonRpcResponse> {
|
||||||
try {
|
try {
|
||||||
const parsed = parseToolName(params.name);
|
const parsed = parseToolName(params.name);
|
||||||
|
|
@ -196,20 +201,49 @@ export async function handleToolsCall(
|
||||||
const actualPackageName = collectionTool.tool.package.npmPackageName;
|
const actualPackageName = collectionTool.tool.package.npmPackageName;
|
||||||
const actualVersion = collectionTool.tool.package.npmVersion;
|
const actualVersion = collectionTool.tool.package.npmVersion;
|
||||||
|
|
||||||
|
// When caller provides env vars (non-owner), validate required env vars
|
||||||
|
if (callerEnvVars !== undefined) {
|
||||||
|
const packageEnv = collectionTool.tool.package.env as TpmjsEnv[] | null;
|
||||||
|
if (packageEnv && Array.isArray(packageEnv)) {
|
||||||
|
const missingVars = packageEnv.filter(
|
||||||
|
(env) => env.required !== false && !env.default && !callerEnvVars[env.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: requestId,
|
||||||
|
error: {
|
||||||
|
code: -32602,
|
||||||
|
message: 'Missing required environment variables',
|
||||||
|
data: {
|
||||||
|
missingVars: missingVars.map((e) => ({
|
||||||
|
name: e.name,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve executor configuration (collection config only for MCP - no agent context)
|
// Resolve executor configuration (collection config only for MCP - no agent context)
|
||||||
const executorConfig = parseExecutorConfig(
|
const executorConfig = parseExecutorConfig(
|
||||||
collection?.executorType,
|
collection?.executorType,
|
||||||
collection?.executorConfig
|
collection?.executorConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute via resolved executor with collection's environment variables
|
// Execute via resolved executor
|
||||||
|
// Use caller-provided env vars if given (non-owner), otherwise use collection's stored env vars
|
||||||
|
const effectiveEnvVars = callerEnvVars ?? (collection?.envVars as Record<string, string>) ?? {};
|
||||||
// Pass explicit version to avoid Deno HTTP import cache issues with @latest
|
// Pass explicit version to avoid Deno HTTP import cache issues with @latest
|
||||||
const result = await executeWithExecutor(executorConfig, {
|
const result = await executeWithExecutor(executorConfig, {
|
||||||
packageName: actualPackageName,
|
packageName: actualPackageName,
|
||||||
name: parsed.toolName,
|
name: parsed.toolName,
|
||||||
version: actualVersion,
|
version: actualVersion,
|
||||||
params: params.arguments ?? {},
|
params: params.arguments ?? {},
|
||||||
env: (collection?.envVars as Record<string, string>) ?? undefined,
|
env: Object.keys(effectiveEnvVars).length > 0 ? effectiveEnvVars : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,8 @@ export const CreateConversationSchema = z.object({
|
||||||
export const SendMessageSchema = z.object({
|
export const SendMessageSchema = z.object({
|
||||||
message: z.string().min(1, 'Message is required').max(50000, 'Message too long'),
|
message: z.string().min(1, 'Message is required').max(50000, 'Message too long'),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
|
// For non-owners accessing public agents: provide your own LLM API key
|
||||||
|
providerApiKey: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue