mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 18:19:44 +00:00
Implement self-hosted MCP server for admin actions, update AI generation tools in admin dashboard, and refine mobile app stores and components.
372 lines
9.7 KiB
TypeScript
372 lines
9.7 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { z } from 'zod';
|
|
import { supabase } from '../lib/supabase.js';
|
|
import { logAudit } from '../utils/audit.js';
|
|
|
|
export function registerContentTools(server: McpServer) {
|
|
// --- Categories ---
|
|
|
|
server.tool(
|
|
'list_categories',
|
|
'List all categories with pack counts',
|
|
{},
|
|
async () => {
|
|
const { data, error } = await supabase
|
|
.from('categories')
|
|
.select(`
|
|
*,
|
|
question_packs (count)
|
|
`)
|
|
.order('sort_order');
|
|
|
|
if (error) throw new Error(`Failed to list categories: ${error.message}`);
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'create_category',
|
|
'Create a new category',
|
|
{
|
|
name: z.string(),
|
|
description: z.string().optional(),
|
|
icon: z.string(),
|
|
sort_order: z.number().optional(),
|
|
is_public: z.boolean().default(false)
|
|
},
|
|
async ({ name, description, icon, sort_order, is_public }) => {
|
|
const { data, error } = await supabase
|
|
.from('categories')
|
|
.insert({ name, description, icon, sort_order: sort_order ?? 0, is_public })
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create category: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'INSERT',
|
|
table_name: 'categories',
|
|
record_id: data.id,
|
|
new_values: data
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'update_category',
|
|
'Update a category',
|
|
{
|
|
id: z.string(),
|
|
name: z.string().optional(),
|
|
description: z.string().optional(),
|
|
icon: z.string().optional(),
|
|
sort_order: z.number().optional(),
|
|
is_public: z.boolean().optional()
|
|
},
|
|
async ({ id, ...updates }) => {
|
|
// Get old values for audit
|
|
const { data: oldData } = await supabase.from('categories').select('*').eq('id', id).single();
|
|
if (!oldData) throw new Error(`Category not found: ${id}`);
|
|
|
|
const { data, error } = await supabase
|
|
.from('categories')
|
|
.update(updates)
|
|
.eq('id', id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to update category: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'UPDATE',
|
|
table_name: 'categories',
|
|
record_id: id,
|
|
old_values: oldData,
|
|
new_values: data
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'delete_category',
|
|
'Delete a category',
|
|
{ id: z.string() },
|
|
async ({ id }) => {
|
|
const { data: oldData } = await supabase.from('categories').select('*').eq('id', id).single();
|
|
if (!oldData) throw new Error(`Category not found: ${id}`);
|
|
|
|
const { error } = await supabase
|
|
.from('categories')
|
|
.delete()
|
|
.eq('id', id);
|
|
|
|
if (error) throw new Error(`Failed to delete category: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'DELETE',
|
|
table_name: 'categories',
|
|
record_id: id,
|
|
old_values: oldData
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: `Category ${id} deleted successfully` }]
|
|
};
|
|
}
|
|
);
|
|
|
|
// --- Packs ---
|
|
|
|
server.tool(
|
|
'list_packs',
|
|
'List packs for a category or all packs',
|
|
{ category_id: z.string().optional() },
|
|
async ({ category_id }) => {
|
|
let query = supabase
|
|
.from('question_packs')
|
|
.select(`
|
|
*,
|
|
questions (count)
|
|
`)
|
|
.order('sort_order');
|
|
|
|
if (category_id) {
|
|
query = query.eq('category_id', category_id);
|
|
}
|
|
|
|
const { data, error } = await query;
|
|
if (error) throw new Error(`Failed to list packs: ${error.message}`);
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'create_pack',
|
|
'Create a new question pack',
|
|
{
|
|
name: z.string(),
|
|
category_id: z.string(),
|
|
description: z.string().optional(),
|
|
is_premium: z.boolean().default(false),
|
|
is_public: z.boolean().default(false),
|
|
is_explicit: z.boolean().default(false),
|
|
sort_order: z.number().optional()
|
|
},
|
|
async (input) => {
|
|
const { data, error } = await supabase
|
|
.from('question_packs')
|
|
.insert({ ...input, sort_order: input.sort_order ?? 0 })
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create pack: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'INSERT',
|
|
table_name: 'question_packs',
|
|
record_id: data.id,
|
|
new_values: data
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'update_pack',
|
|
'Update a question pack',
|
|
{
|
|
id: z.string(),
|
|
name: z.string().optional(),
|
|
category_id: z.string().optional(),
|
|
description: z.string().optional(),
|
|
is_premium: z.boolean().optional(),
|
|
is_public: z.boolean().optional(),
|
|
is_explicit: z.boolean().optional(),
|
|
sort_order: z.number().optional()
|
|
},
|
|
async ({ id, ...updates }) => {
|
|
const { data: oldData } = await supabase.from('question_packs').select('*').eq('id', id).single();
|
|
if (!oldData) throw new Error(`Pack not found: ${id}`);
|
|
|
|
const { data, error } = await supabase
|
|
.from('question_packs')
|
|
.update(updates)
|
|
.eq('id', id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to update pack: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'UPDATE',
|
|
table_name: 'question_packs',
|
|
record_id: id,
|
|
old_values: oldData,
|
|
new_values: data
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'delete_pack',
|
|
'Delete a question pack',
|
|
{ id: z.string() },
|
|
async ({ id }) => {
|
|
const { data: oldData } = await supabase.from('question_packs').select('*').eq('id', id).single();
|
|
if (!oldData) throw new Error(`Pack not found: ${id}`);
|
|
|
|
const { error } = await supabase.from('question_packs').delete().eq('id', id);
|
|
if (error) throw new Error(`Failed to delete pack: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'DELETE',
|
|
table_name: 'question_packs',
|
|
record_id: id,
|
|
old_values: oldData
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: `Pack ${id} deleted successfully` }]
|
|
};
|
|
}
|
|
);
|
|
|
|
// --- Questions ---
|
|
|
|
server.tool(
|
|
'list_questions',
|
|
'List questions for a pack',
|
|
{ pack_id: z.string() },
|
|
async ({ pack_id }) => {
|
|
const { data, error } = await supabase
|
|
.from('questions')
|
|
.select('*')
|
|
.eq('pack_id', pack_id)
|
|
.order('created_at'); // Questions don't strictly have a sort_order usually
|
|
|
|
if (error) throw new Error(`Failed to list questions: ${error.message}`);
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'create_question',
|
|
'Create a single question',
|
|
{
|
|
pack_id: z.string(),
|
|
text: z.string(),
|
|
partner_text: z.string().optional(),
|
|
intensity: z.number().min(1).max(5).default(1)
|
|
},
|
|
async (input) => {
|
|
const { data, error } = await supabase
|
|
.from('questions')
|
|
.insert(input)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to create question: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'INSERT',
|
|
table_name: 'questions',
|
|
record_id: data.id,
|
|
new_values: data
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'update_question',
|
|
'Update a question',
|
|
{
|
|
id: z.string(),
|
|
text: z.string().optional(),
|
|
partner_text: z.string().optional(),
|
|
intensity: z.number().min(1).max(5).optional(),
|
|
pack_id: z.string().optional()
|
|
},
|
|
async ({ id, ...updates }) => {
|
|
const { data: oldData } = await supabase.from('questions').select('*').eq('id', id).single();
|
|
if (!oldData) throw new Error(`Question not found: ${id}`);
|
|
|
|
const { data, error } = await supabase
|
|
.from('questions')
|
|
.update(updates)
|
|
.eq('id', id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw new Error(`Failed to update question: ${error.message}`);
|
|
|
|
await logAudit({
|
|
action: 'UPDATE',
|
|
table_name: 'questions',
|
|
record_id: id,
|
|
old_values: oldData,
|
|
new_values: data
|
|
});
|
|
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
};
|
|
}
|
|
);
|
|
|
|
server.tool(
|
|
'delete_questions',
|
|
'Delete one or more questions',
|
|
{ ids: z.array(z.string()) },
|
|
async ({ ids }) => {
|
|
const { data: oldData } = await supabase.from('questions').select('*').in('id', ids);
|
|
|
|
const { error } = await supabase.from('questions').delete().in('id', ids);
|
|
if (error) throw new Error(`Failed to delete questions: ${error.message}`);
|
|
|
|
// Log audit for each deleted question
|
|
if (oldData) {
|
|
for (const record of oldData) {
|
|
await logAudit({
|
|
action: 'DELETE',
|
|
table_name: 'questions',
|
|
record_id: record.id,
|
|
old_values: record
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
content: [{ type: 'text', text: `${ids.length} questions deleted successfully` }]
|
|
};
|
|
}
|
|
);
|
|
}
|