Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/shared/tools/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export function registerDatabaseTools(ctx: RegisterContext): void {
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
tableName: z.string().describe('Name of the table'),
},
{
title: 'Get Table Schema',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('get-table-schema', async ({ apiKey, tableName }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down Expand Up @@ -119,6 +126,13 @@ export function registerDatabaseTools(ctx: RegisterContext): void {
{
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
},
{
title: 'Get Backend Metadata',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('get-backend-metadata', async ({ apiKey }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down Expand Up @@ -148,6 +162,13 @@ export function registerDatabaseTools(ctx: RegisterContext): void {
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
...rawSQLRequestSchema.shape,
},
{
title: 'Run Raw SQL',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking('run-raw-sql', async ({ apiKey, query, params }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down Expand Up @@ -185,6 +206,13 @@ export function registerDatabaseTools(ctx: RegisterContext): void {
frame: z.enum(['react', 'nextjs']).describe('Framework to use for the template (support React and Next.js)'),
projectName: z.string().optional().describe('Name for the project directory (optional, defaults to "insforge-{frame}")'),
},
{
title: 'Download Starter Template',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
withUsageTracking('download-template', async ({ frame, projectName }) => {
try {
const response = await fetch(`${API_BASE_URL}/api/auth/tokens/anon`, {
Expand Down Expand Up @@ -238,6 +266,13 @@ After the command completes, \`cd ${shellEsc(targetDir)}\` and start developing.
frame: z.enum(['react', 'nextjs']).describe('Framework to use for the template (support React and Next.js)'),
projectName: z.string().optional().describe('Name for the project directory (optional, defaults to "insforge-{frame}")'),
},
{
title: 'Download Starter Template',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking('download-template', async ({ frame, projectName }) => {
try {
const response = await fetch(`${API_BASE_URL}/api/auth/tokens/anon`, {
Expand Down Expand Up @@ -314,6 +349,13 @@ To: Your current project directory
...bulkUpsertRequestSchema.shape,
filePath: z.string().describe('Path to CSV or JSON file containing data to import'),
},
{
title: 'Bulk Upsert Data',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('bulk-upsert', async ({ apiKey, table, filePath, upsertKey }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down
28 changes: 28 additions & 0 deletions src/shared/tools/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,13 @@ export function registerDeploymentTools(ctx: RegisterContext): void {
.describe('Log source to retrieve'),
limit: z.number().optional().default(20).describe('Number of logs to return (default: 20)'),
},
{
title: 'Get Container Logs',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('get-container-logs', async ({ apiKey, source, limit }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down Expand Up @@ -565,6 +572,13 @@ export function registerDeploymentTools(ctx: RegisterContext): void {
'Absolute path to the source directory containing files to deploy (e.g., /Users/name/project). Do not use relative paths like "."'
),
},
{
title: 'Create Deployment',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking('create-deployment', async ({ sourceDirectory }) => {
try {
if (!isAbsoluteSourcePath(sourceDirectory)) {
Expand Down Expand Up @@ -659,6 +673,13 @@ Run each step in order. If any step fails, do not proceed to the next step.`;
deploymentId: z.string().describe('The deployment ID returned by create-deployment'),
...startDeploymentRequestSchema.shape,
},
{
title: 'Start Deployment',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking(
'start-deployment',
async ({ deploymentId, projectSettings, envVars, meta }) => {
Expand Down Expand Up @@ -720,6 +741,13 @@ Run each step in order. If any step fails, do not proceed to the next step.`;
),
...startDeploymentRequestSchema.shape,
},
{
title: 'Create Deployment',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking(
'create-deployment',
async ({ sourceDirectory, projectSettings, envVars, meta }) => {
Expand Down
21 changes: 21 additions & 0 deletions src/shared/tools/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export function registerDocsTools(ctx: RegisterContext): void {
'fetch-docs',
'Fetch Insforge documentation. Use "instructions" for essential backend setup (MANDATORY FIRST), or select specific SDK docs for database, auth, storage, functions, or AI integration.',
{ docType: docTypeSchema },
{
title: 'Fetch Documentation',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('fetch-docs', async ({ docType }) => {
try {
const content = await fetchDocumentation(docType);
Expand Down Expand Up @@ -104,6 +111,13 @@ export function registerDocsTools(ctx: RegisterContext): void {
Supported features: ${sdkFeatureSchema.options.join(', ')}
Supported languages: ${sdkLanguageSchema.options.join(', ')}`,
{ sdkFeature: sdkFeatureSchema, sdkLanguage: sdkLanguageSchema },
{
title: 'Fetch SDK Documentation',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('fetch-sdk-docs', async ({ sdkFeature, sdkLanguage }) => {
try {
const content = await fetchSDKDocumentation(sdkFeature, sdkLanguage);
Expand Down Expand Up @@ -134,6 +148,13 @@ Supported languages: ${sdkLanguageSchema.options.join(', ')}`,
{
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
},
{
title: 'Get Anonymous Key',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking('get-anon-key', async ({ apiKey }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down
42 changes: 42 additions & 0 deletions src/shared/tools/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export function registerFunctionTools(ctx: RegisterContext): void {
'The function code as a string. Must export: module.exports = async function(request) { return new Response(...) }'
),
},
{
title: 'Create Edge Function',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withUsageTracking('create-function', async (args: any) => {
try {
Expand Down Expand Up @@ -72,6 +79,13 @@ export function registerFunctionTools(ctx: RegisterContext): void {
'Path to JavaScript file containing the function code. Must export: module.exports = async function(request) { return new Response(...) }'
),
},
{
title: 'Create Edge Function',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withUsageTracking('create-function', async (args: any) => {
try {
Expand Down Expand Up @@ -122,6 +136,13 @@ export function registerFunctionTools(ctx: RegisterContext): void {
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
slug: functionSchema.shape.slug.describe('The slug identifier of the function'),
},
{
title: 'Get Edge Function',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withUsageTracking('get-function', async (args: any) => {
try {
Expand Down Expand Up @@ -157,6 +178,13 @@ export function registerFunctionTools(ctx: RegisterContext): void {
'The new function code as a string. Must export: module.exports = async function(request) { return new Response(...) }'
),
},
{
title: 'Update Edge Function',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withUsageTracking('update-function', async (args: any) => {
try {
Expand Down Expand Up @@ -201,6 +229,13 @@ export function registerFunctionTools(ctx: RegisterContext): void {
'Path to JavaScript file containing the new function code. Must export: module.exports = async function(request) { return new Response(...) }'
),
},
{
title: 'Update Edge Function',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withUsageTracking('update-function', async (args: any) => {
try {
Expand Down Expand Up @@ -254,6 +289,13 @@ export function registerFunctionTools(ctx: RegisterContext): void {
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
slug: functionSchema.shape.slug.describe('The slug identifier of the function to delete'),
},
{
title: 'Delete Edge Function',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withUsageTracking('delete-function', async (args: any) => {
try {
Expand Down
52 changes: 32 additions & 20 deletions src/shared/tools/integration-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actual = await vi.importActual('node-fetch') as any;
return {
default: vi.fn(async (url: string | URL, init?: any) => {

Check warning on line 8 in src/shared/tools/integration-bridge.test.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
if (url.toString().endsWith('/api/health')) {
return {
ok: true,
Expand All @@ -26,29 +26,22 @@
const mockServer = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tool: (name: string, ...args: any[]) => {
let description = '';
// The handler callback is always the final argument. Preceding args are
// any combination of description (string), input schema (object), and
// annotations (object), matching the SDK's `tool(...)` overloads.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let schema: any = {};
const cb: any = args[args.length - 1];
const rest = args.slice(0, -1);

const description = typeof rest[0] === 'string' ? (rest[0] as string) : '';
const objectArgs = rest.filter((arg) => typeof arg === 'object' && arg !== null);
// First object arg is the input schema; a second object arg (if any) is annotations.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let cb: any;

if (args.length === 1) {
cb = args[0];
} else if (args.length === 2) {
if (typeof args[0] === 'string') {
description = args[0];
cb = args[1];
} else {
schema = args[0];
cb = args[1];
}
} else if (args.length >= 3) {
description = args[0];
schema = args[1];
cb = args[2];
}
const schema: any = objectArgs[0] ?? {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const annotations: any = objectArgs[1];

registeredTools.set(name, { description, schema, cb });
registeredTools.set(name, { description, schema, annotations, cb });
return mockServer;
}
};
Expand All @@ -65,7 +58,7 @@
apiBaseUrl,
mode: 'local'
});
} catch (e: any) {

Check warning on line 61 in src/shared/tools/integration-bridge.test.ts

View workflow job for this annotation

GitHub Actions / verify

Unexpected any. Specify a different type
console.warn('Tool registration encountered warning (may be offline / unreachable backend):', e.message);
}
});
Expand All @@ -75,6 +68,25 @@
expect(registeredTools.has('fetch-docs')).toBe(true);
});

it('should attach MCP annotations to registered tools', () => {
// Read-only tool: should advertise readOnlyHint and a human-readable title.
const fetchDocs = registeredTools.get('fetch-docs');
expect(fetchDocs?.annotations).toBeDefined();
expect(fetchDocs.annotations.title).toBe('Fetch Documentation');
expect(fetchDocs.annotations.readOnlyHint).toBe(true);
expect(fetchDocs.annotations.destructiveHint).toBe(false);

// Destructive tool: run-raw-sql can run arbitrary SQL (incl. DROP).
const runRawSql = registeredTools.get('run-raw-sql');
expect(runRawSql?.annotations).toBeDefined();
expect(runRawSql.annotations.readOnlyHint).toBe(false);
expect(runRawSql.annotations.destructiveHint).toBe(true);

// The handler must still be the captured callback, not the annotations object.
expect(typeof fetchDocs.cb).toBe('function');
expect(typeof runRawSql.cb).toBe('function');
});

it('should execute fetch-docs tool for instructions if credentials setup', async () => {
const tool = registeredTools.get('fetch-docs');
expect(tool).toBeDefined();
Expand Down
21 changes: 21 additions & 0 deletions src/shared/tools/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export function registerStorageTools(ctx: RegisterContext): void {
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
...createBucketRequestSchema.shape,
},
{
title: 'Create Storage Bucket',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
withUsageTracking('create-bucket', async ({ apiKey, bucketName, isPublic }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down Expand Up @@ -50,6 +57,13 @@ export function registerStorageTools(ctx: RegisterContext): void {
{
apiKey: z.string().optional().describe('API key for authentication (optional if provided via --api_key)'),
},
{
title: 'List Storage Buckets',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('list-buckets', async ({ apiKey }) => {
try {
const response = await fetch(`${API_BASE_URL}/api/storage/buckets`, {
Expand Down Expand Up @@ -79,6 +93,13 @@ export function registerStorageTools(ctx: RegisterContext): void {
// Reuse the same bucket name validation as create-bucket
bucketName: createBucketRequestSchema.shape.bucketName,
},
{
title: 'Delete Storage Bucket',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: true,
},
withUsageTracking('delete-bucket', async ({ apiKey, bucketName }) => {
try {
const actualApiKey = getApiKey(apiKey);
Expand Down
Loading