mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 18:19:44 +00:00
Add MCP server and enhance AI content tools
Implement self-hosted MCP server for admin actions, update AI generation tools in admin dashboard, and refine mobile app stores and components.
This commit is contained in:
1 parent
b0f1700e40
commit
4e40b92b16
71 files changed
+10511
-1008
No files matched your search
@@ -0,0 +1,371 @@
|
||||
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` }]
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user