feat: add MCP server with resources, tools, auth, and Traefik routing

- Add backend/app/mcp_server.py with FastMCP server (hms-cms)
- Resources: design-rules, page-structure, sync-status
- Tools: trigger_sync, get_sync_log, set_rentman_token, read_file,
  write_file, create_page, deploy, git_commit, git_status
- Bearer token auth via MCP_AUTH_TOKEN env var
- Mount MCP at /mcp with streamable HTTP transport
- Integrate session manager in FastAPI lifespan
- Add Traefik labels for external /mcp route on hms.media-on.de
- Add git, docker.io, curl to backend Dockerfile
- Mount repo root + docker socket in backend container
- Add mcp>=1.0.0 to requirements.txt
This commit is contained in:
Agent Zero
2026-07-12 15:58:22 +02:00
parent 00c23bc066
commit cd465e0564
124 changed files with 3309 additions and 225 deletions
+55
View File
@@ -0,0 +1,55 @@
import type { $Fetch } from "nitropack";
/**
* Base API client composable.
* Uses server-side apiBase for SSR requests, public apiBase for client-side.
*/
export function useApi() {
const config = useRuntimeConfig();
// On server: config.apiBase (http://backend:8000)
// On client: config.public.apiBase (/api)
const apiBase = import.meta.server ? config.apiBase : config.public.apiBase;
const client: $Fetch = $fetch.create({
baseURL: apiBase,
headers: {
"Content-Type": "application/json",
},
});
async function get<T>(url: string, params?: Record<string, string | number | boolean | undefined>): Promise<T> {
return client<T>(url, {
method: "GET",
params: params as Record<string, string> | undefined,
});
}
async function post<T>(url: string, body?: unknown): Promise<T> {
return client<T>(url, {
method: "POST",
body,
});
}
async function put<T>(url: string, body?: unknown): Promise<T> {
return client<T>(url, {
method: "PUT",
body,
});
}
async function del<T>(url: string): Promise<T> {
return client<T>(url, {
method: "DELETE",
});
}
return {
apiBase,
client,
get,
post,
put,
del,
};
}
+60
View File
@@ -0,0 +1,60 @@
import { useCartStore } from "~/stores/cart";
import { storeToRefs } from "pinia";
import type { EquipmentItem } from "~/composables/useEquipment";
/**
* Cart composable wraps the Pinia cart store for convenient use in components.
* Provides reactive refs and action methods.
*/export function useCart() {
const store = useCartStore();
const { items, totalCount, isEmpty, hasItems, apiItems } = storeToRefs(store);
function addItem(equipment: EquipmentItem): void {
store.addItem(equipment);
}
function addItemByFields(item: {
equipment_id: number;
name: string;
rental_price: number | null;
image_url: string | null;
}): void {
store.addItemByFields(item);
}
function removeItem(equipmentId: number): void {
store.removeItem(equipmentId);
}
function updateQuantity(equipmentId: number, quantity: number): void {
store.updateQuantity(equipmentId, quantity);
}
function incrementQuantity(equipmentId: number): void {
store.incrementQuantity(equipmentId);
}
function decrementQuantity(equipmentId: number): void {
store.decrementQuantity(equipmentId);
}
function clearCart(): void {
store.clearCart();
}
return {
items,
totalCount,
isEmpty,
hasItems,
apiItems,
addItem,
addItemByFields,
removeItem,
updateQuantity,
incrementQuantity,
decrementQuantity,
clearCart,
};
}
@@ -0,0 +1,83 @@
/**
* Equipment API composable.
* Wraps useApi to provide typed equipment list, detail, and categories endpoints.
*/
export interface EquipmentItem {
id: number;
rentman_id: string;
name: string;
number: string | null;
category: string | null;
description: string | null;
image_url: string | null;
rental_price: number | null;
available: boolean;
}
export interface EquipmentDetail {
id: number;
rentman_id: string;
name: string;
number: string | null;
category: string | null;
description: string | null;
specifications: Record<string, unknown> | null;
images: string[] | null;
rental_price: number | null;
brand: string | null;
available: boolean;
}
export interface PaginatedEquipment {
items: EquipmentItem[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
export type SortOption = "name_asc" | "name_desc";
export function useEquipment() {
const api = useApi();
/**
* Fetch paginated equipment list with search, category filter, and sort.
*/
async function list(params: {
search?: string;
category?: string;
sort?: SortOption;
page?: number;
page_size?: number;
} = {}): Promise<PaginatedEquipment> {
return api.get<PaginatedEquipment>("/api/equipment", {
search: params.search || undefined,
category: params.category || undefined,
sort: params.sort || "name_asc",
page: params.page || 1,
page_size: params.page_size || 20,
});
}
/**
* Fetch a single equipment detail by ID.
*/
async function detail(id: number | string): Promise<EquipmentDetail> {
return api.get<EquipmentDetail>(`/api/equipment/${id}`);
}
/**
* Fetch all available equipment categories.
*/
async function categories(): Promise<string[]> {
return api.get<string[]>("/api/equipment/categories");
}
return {
list,
detail,
categories,
};
}