Browse Source

fix: 修复 Admin MCP 地区树工具并添加公司/地区管理工具

- 修复 admin_get_area_tree 处理后端返回的包装格式 { success, data }
- Area 类型添加 isDisabled 字段以兼容后端 tree 接口
- 添加地区管理工具: create, update, delete, list, get, get_tree
- 添加公司管理工具: create, update, delete, list, get

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 3 tuần trước cách đây
mục cha
commit
69b1a93748

+ 275 - 0
packages/admin-mcp-server/src/index.ts

@@ -17,6 +17,8 @@ import { userTools } from './tools/user-tools.js';
 import { roleTools } from './tools/role-tools.js';
 import { systemConfigTools } from './tools/system-config-tools.js';
 import { orderTools } from './tools/order-tools.js';
+import { areaTools } from './tools/area-tools.js';
+import { companyTools } from './tools/company-tools.js';
 import { authTools } from './tools/auth-tools.js';
 import { getApiClient } from './services/api-client.js';
 
@@ -574,6 +576,279 @@ Error Handling:
   - Returns "Error: Resource not found" if order ID doesn't exist`,
     targetServer
   );
+
+  // Area management tools
+  registerToolWithAnnotations(
+    areaTools.areaList,
+    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
+    'List Areas',
+    `List and search geographic areas (provinces, cities, districts) with pagination.
+
+This tool retrieves a paginated list of areas from the admin system.
+Supports keyword search, sorting, and filtering.
+
+Args:
+  - page (number): Page number, starting from 1 (default: 1)
+  - pageSize (number): Number of items per page, 1-100 (default: 20)
+  - keyword (string): Optional search keyword
+  - sortBy (string): Optional field to sort by
+  - sortOrder (string): Sort direction: "ASC" or "DESC" (default: "DESC")
+  - filters (string): Optional filter conditions as JSON string
+  - response_format (string): Output format: "markdown" or "json" (default: "markdown")
+
+Returns:
+  Paginated list of areas with id, name, code, level, parentId, status.
+
+Examples:
+  - Use when: "List all areas" -> params with page=1, pageSize=20
+
+Error Handling:
+  - Returns "Error: Permission denied" if not authorized`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    areaTools.areaGet,
+    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
+    'Get Area by ID',
+    `Retrieve detailed information about a specific area by ID.
+
+Args:
+  - id (number): Area ID (required)
+
+Returns:
+  Complete area information.
+
+Examples:
+  - Use when: "Get area with ID 1" -> params with id=1
+
+Error Handling:
+  - Returns "Error: Resource not found" if area ID doesn't exist`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    areaTools.areaGetTree,
+    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
+    'Get Area Tree',
+    `Get the complete hierarchical tree structure of all geographic areas.
+
+This tool returns areas organized as a tree (provinces -> cities -> districts).
+Useful for displaying region selectors or analyzing geographic hierarchy.
+
+Args:
+  - None
+
+Returns:
+  Hierarchical tree structure with nested children arrays.
+
+Examples:
+  - Use when: "Get all provinces and cities" or "Show area tree"
+
+Error Handling:
+  - Returns "Error: Permission denied" if not authorized`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    areaTools.areaCreate,
+    { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
+    'Create Area',
+    `Create a new geographic area (province, city, or district).
+
+Args:
+  - name (string): Area name (required)
+  - code (string): Area code e.g. BJS, ZJS (required)
+  - level (number): Area level: 1=province, 2=city, 3=district (required)
+  - parentId (number): Parent area ID (required for cities and districts)
+  - status (number): Status: 0 = disabled, 1 = enabled (optional)
+
+Returns:
+  Created area information with ID.
+
+Examples:
+  - Use when: "Create a new province" -> params with name="New Province", code="NWP", level=1
+
+Error Handling:
+  - Returns "Error: Data already exists" if code is duplicated`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    areaTools.areaUpdate,
+    { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
+    'Update Area',
+    `Update an existing geographic area.
+
+Args:
+  - id (number): Area ID to update (required)
+  - name (string): Area name (optional)
+  - code (string): Area code (optional)
+  - level (number): Area level (optional)
+  - status (number): Status: 0 = disabled, 1 = enabled (optional)
+
+Returns:
+  Updated area information.
+
+Examples:
+  - Use when: "Update area name" -> params with id=1, name="New Name"
+
+Error Handling:
+  - Returns "Error: Resource not found" if area ID doesn't exist`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    areaTools.areaDelete,
+    { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
+    'Delete Area',
+    `Delete a geographic area by ID.
+
+Args:
+  - id (number): Area ID to delete (required)
+
+Returns:
+  Deletion confirmation.
+
+Examples:
+  - Use when: "Delete area with ID 10" -> params with id=10
+
+Error Handling:
+  - Returns "Error: Resource not found" if area ID doesn't exist`,
+    targetServer
+  );
+
+  // Company management tools
+  registerToolWithAnnotations(
+    companyTools.companyList,
+    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
+    'List Companies',
+    `List and search companies with pagination and filtering.
+
+This tool retrieves a paginated list of companies from the admin system.
+Supports keyword search, sorting, and filtering.
+
+Args:
+  - page (number): Page number, starting from 1 (default: 1)
+  - pageSize (number): Number of items per page, 1-100 (default: 20)
+  - keyword (string): Optional search keyword for company name
+  - sortBy (string): Optional field to sort by
+  - sortOrder (string): Sort direction: "ASC" or "DESC" (default: "DESC")
+  - filters (string): Optional filter conditions as JSON string
+  - response_format (string): Output format: "markdown" or "json" (default: "markdown")
+
+Returns:
+  For JSON format: Structured data with schema:
+  {
+    "total": number,
+    "count": number,
+    "current": number,
+    "pageSize": number,
+    "companies": [...],
+    "has_more": boolean
+  }
+
+Examples:
+  - Use when: "List all companies" -> params with page=1, pageSize=20
+  - Use when: "Search for company Zhongzhi" -> params with keyword="Zhongzhi"
+
+Error Handling:
+  - Returns "Error: Permission denied" if not authorized`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    companyTools.companyGet,
+    { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
+    'Get Company by ID',
+    `Retrieve detailed information about a specific company by ID.
+
+Args:
+  - id (number): Company ID (required)
+
+Returns:
+  Complete company information.
+
+Examples:
+  - Use when: "Get company with ID 1" -> params with id=1
+
+Error Handling:
+  - Returns "Error: Resource not found" if company ID doesn't exist`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    companyTools.companyCreate,
+    { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
+    'Create Company',
+    `Create a new company in the admin system.
+
+Args:
+  - companyName (string): Company name (required)
+  - contactPerson (string): Contact person name (optional)
+  - contactPhone (string): Contact phone number (optional)
+  - contactEmail (string): Contact email (optional)
+  - address (string): Company address (optional)
+  - platformId (number): Platform ID (optional)
+  - status (number): Status: 0 = disabled, 1 = enabled (optional)
+
+Returns:
+  Created company information with ID.
+
+Examples:
+  - Use when: "Create a new company" -> params with companyName="ABC Corp"
+
+Error Handling:
+  - Returns "Error: Data already exists" if company name is duplicated`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    companyTools.companyUpdate,
+    { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
+    'Update Company',
+    `Update an existing company.
+
+Args:
+  - id (number): Company ID to update (required)
+  - companyName (string): Company name (optional)
+  - contactPerson (string): Contact person name (optional)
+  - contactPhone (string): Contact phone number (optional)
+  - contactEmail (string): Contact email (optional)
+  - address (string): Company address (optional)
+  - platformId (number): Platform ID (optional)
+  - status (number): Status: 0 = disabled, 1 = enabled (optional)
+
+Returns:
+  Updated company information.
+
+Examples:
+  - Use when: "Update company contact" -> params with id=1, contactPerson="John Doe"
+
+Error Handling:
+  - Returns "Error: Resource not found" if company ID doesn't exist`,
+    targetServer
+  );
+
+  registerToolWithAnnotations(
+    companyTools.companyDelete,
+    { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
+    'Delete Company',
+    `Delete a company by ID.
+
+Args:
+  - id (number): Company ID to delete (required)
+
+Returns:
+  Deletion confirmation.
+
+Examples:
+  - Use when: "Delete company with ID 10" -> params with id=10
+
+Error Handling:
+  - Returns "Error: Resource not found" if company ID doesn't exist`,
+    targetServer
+  );
 }
 
 // ============================================================================

+ 222 - 0
packages/admin-mcp-server/src/schemas/index.ts

@@ -448,6 +448,218 @@ export const OrderListInputSchema = ListQuerySchema;
  */
 export const OrderGetInputSchema = ResourceIdSchema;
 
+// ============================================================================
+// Area Schemas
+// ============================================================================
+
+/**
+ * Area list input schema
+ */
+export const AreaListInputSchema = ListQuerySchema;
+
+/**
+ * Area get input schema
+ */
+export const AreaGetInputSchema = ResourceIdSchema;
+
+/**
+ * Area create input schema
+ */
+export const AreaCreateInputSchema = z
+  .object({
+    name: z
+      .string()
+      .min(1, 'Area name is required')
+      .max(100, 'Area name cannot exceed 100 characters')
+      .describe('Area name (required)'),
+    code: z
+      .string()
+      .min(1, 'Area code is required')
+      .max(20, 'Area code cannot exceed 20 characters')
+      .describe('Area code (e.g., BJS, ZJS)'),
+    level: z
+      .number()
+      .int()
+      .min(1, 'Level must be at least 1')
+      .max(3, 'Level cannot exceed 3')
+      .describe('Area level: 1=province, 2=city, 3=district'),
+    parentId: z
+      .number()
+      .int()
+      .positive()
+      .optional()
+      .describe('Parent area ID (required for cities and districts)'),
+    status: z
+      .number()
+      .int()
+      .min(0)
+      .max(1)
+      .optional()
+      .describe('Status: 0 = disabled, 1 = enabled')
+  })
+  .strict();
+
+/**
+ * Area update input schema
+ */
+export const AreaUpdateInputSchema = z
+  .object({
+    id: z
+      .number()
+      .int()
+      .positive('ID must be a positive integer')
+      .describe('Area ID to update'),
+    name: z
+      .string()
+      .min(1, 'Area name is required')
+      .max(100, 'Area name cannot exceed 100 characters')
+      .optional()
+      .describe('Area name'),
+    code: z
+      .string()
+      .min(1, 'Area code is required')
+      .max(20, 'Area code cannot exceed 20 characters')
+      .optional()
+      .describe('Area code'),
+    level: z
+      .number()
+      .int()
+      .min(1)
+      .max(3)
+      .optional()
+      .describe('Area level'),
+    status: z
+      .number()
+      .int()
+      .min(0)
+      .max(1)
+      .optional()
+      .describe('Status: 0 = disabled, 1 = enabled')
+  })
+  .strict();
+
+/**
+ * Area delete input schema
+ */
+export const AreaDeleteInputSchema = ResourceIdSchema;
+
+// ============================================================================
+// Company Schemas
+// ============================================================================
+
+/**
+ * Company list input schema
+ */
+export const CompanyListInputSchema = ListQuerySchema;
+
+/**
+ * Company get input schema
+ */
+export const CompanyGetInputSchema = ResourceIdSchema;
+
+/**
+ * Company create input schema
+ */
+export const CompanyCreateInputSchema = z
+  .object({
+    companyName: z
+      .string()
+      .min(1, 'Company name is required')
+      .max(200, 'Company name cannot exceed 200 characters')
+      .describe('Company name (required)'),
+    contactPerson: z
+      .string()
+      .max(50, 'Contact person cannot exceed 50 characters')
+      .optional()
+      .describe('Contact person name'),
+    contactPhone: z
+      .string()
+      .regex(/^1[3-9]\d{9}$/, 'Invalid phone number format')
+      .optional()
+      .describe('Contact phone number'),
+    contactEmail: z
+      .string()
+      .email('Invalid email format')
+      .optional()
+      .describe('Contact email'),
+    address: z
+      .string()
+      .max(500, 'Address cannot exceed 500 characters')
+      .optional()
+      .describe('Company address'),
+    platformId: z
+      .number()
+      .int()
+      .positive()
+      .optional()
+      .describe('Platform ID'),
+    status: z
+      .number()
+      .int()
+      .min(0)
+      .max(1)
+      .optional()
+      .describe('Status: 0 = disabled, 1 = enabled')
+  })
+  .strict();
+
+/**
+ * Company update input schema
+ */
+export const CompanyUpdateInputSchema = z
+  .object({
+    id: z
+      .number()
+      .int()
+      .positive('ID must be a positive integer')
+      .describe('Company ID to update'),
+    companyName: z
+      .string()
+      .min(1, 'Company name is required')
+      .max(200, 'Company name cannot exceed 200 characters')
+      .optional()
+      .describe('Company name'),
+    contactPerson: z
+      .string()
+      .max(50, 'Contact person cannot exceed 50 characters')
+      .optional()
+      .describe('Contact person name'),
+    contactPhone: z
+      .string()
+      .regex(/^1[3-9]\d{9}$/, 'Invalid phone number format')
+      .optional()
+      .describe('Contact phone number'),
+    contactEmail: z
+      .string()
+      .email('Invalid email format')
+      .optional()
+      .describe('Contact email'),
+    address: z
+      .string()
+      .max(500, 'Address cannot exceed 500 characters')
+      .optional()
+      .describe('Company address'),
+    platformId: z
+      .number()
+      .int()
+      .positive()
+      .optional()
+      .describe('Platform ID'),
+    status: z
+      .number()
+      .int()
+      .min(0)
+      .max(1)
+      .optional()
+      .describe('Status: 0 = disabled, 1 = enabled')
+  })
+  .strict();
+
+/**
+ * Company delete input schema
+ */
+export const CompanyDeleteInputSchema = ResourceIdSchema;
+
 // ============================================================================
 // Export all schema types
 // ============================================================================
@@ -471,3 +683,13 @@ export type SystemConfigUpdateInput = z.infer<typeof SystemConfigUpdateInputSche
 export type SystemConfigDeleteInput = z.infer<typeof SystemConfigDeleteInputSchema>;
 export type OrderListInput = z.infer<typeof OrderListInputSchema>;
 export type OrderGetInput = z.infer<typeof OrderGetInputSchema>;
+export type AreaListInput = z.infer<typeof AreaListInputSchema>;
+export type AreaGetInput = z.infer<typeof AreaGetInputSchema>;
+export type AreaCreateInput = z.infer<typeof AreaCreateInputSchema>;
+export type AreaUpdateInput = z.infer<typeof AreaUpdateInputSchema>;
+export type AreaDeleteInput = z.infer<typeof AreaDeleteInputSchema>;
+export type CompanyListInput = z.infer<typeof CompanyListInputSchema>;
+export type CompanyGetInput = z.infer<typeof CompanyGetInputSchema>;
+export type CompanyCreateInput = z.infer<typeof CompanyCreateInputSchema>;
+export type CompanyUpdateInput = z.infer<typeof CompanyUpdateInputSchema>;
+export type CompanyDeleteInput = z.infer<typeof CompanyDeleteInputSchema>;

+ 391 - 0
packages/admin-mcp-server/src/tools/area-tools.ts

@@ -0,0 +1,391 @@
+/**
+ * Area Management Tools
+ *
+ * MCP tools for managing geographic areas (provinces, cities, districts) in the admin system.
+ */
+
+import { getApiClient } from '../services/api-client.js';
+import {
+  AreaListInputSchema,
+  AreaGetInputSchema,
+  AreaCreateInputSchema,
+  AreaUpdateInputSchema,
+  AreaDeleteInputSchema
+} from '../schemas/index.js';
+import type {
+  AreaListInput,
+  AreaGetInput,
+  AreaCreateInput,
+  AreaUpdateInput,
+  AreaDeleteInput
+} from '../schemas/index.js';
+import { CHARACTER_LIMIT, ResponseFormat } from '../constants.js';
+import type { Area, PaginatedResponse } from '../types.js';
+
+/**
+ * Format area data for markdown output
+ */
+function formatAreaMarkdown(area: Area): string {
+  const levelNames: Record<number, string> = {
+    1: 'Province',
+    2: 'City',
+    3: 'District'
+  };
+
+  const lines = [
+    `## Area: ${area.name} (ID: ${area.id})`,
+    '',
+    `**Name**: ${area.name}`,
+    `**Code**: ${area.code}`,
+    `**Level**: ${levelNames[area.level] || area.level}`,
+    area.parentId ? `**Parent ID**: ${area.parentId}` : null,
+    `**Status**: ${area.status === 1 ? 'Enabled' : 'Disabled'}`,
+    `**Created At**: ${new Date(area.createdAt).toLocaleString('zh-CN')}`,
+    ''
+  ].filter(Boolean);
+
+  return lines.join('\n');
+}
+
+/**
+ * Format area list for markdown output
+ */
+function formatAreaListMarkdown(areas: Area[], total: number, current: number, pageSize: number): string {
+  const lines = [
+    `# Area List`,
+    '',
+    `Total: ${total} areas | Page: ${current} | Page Size: ${pageSize}`,
+    ''
+  ];
+
+  for (const area of areas) {
+    lines.push(formatAreaMarkdown(area));
+  }
+
+  return lines.join('\n');
+}
+
+/**
+ * Format area tree for markdown output
+ */
+function formatAreaTreeMarkdown(areas: Area[], indent: number = 0): string {
+  const lines: string[] = [];
+  const prefix = '  '.repeat(indent);
+
+  for (const area of areas) {
+    const statusIcon = area.status === 1 ? '✅' : '❌';
+    lines.push(`${prefix}${statusIcon} **${area.name}** (${area.code}) [ID: ${area.id}]`);
+
+    if (area.children && area.children.length > 0) {
+      lines.push(formatAreaTreeMarkdown(area.children, indent + 1));
+    }
+  }
+
+  return lines.join('\n');
+}
+
+/**
+ * List areas tool
+ */
+export const areaListTool = async (args: AreaListInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { page, pageSize, keyword, sortBy, sortOrder, filters, response_format } = args;
+
+    const params: Record<string, unknown> = {
+      page: page || 1,
+      pageSize: pageSize || 20
+    };
+
+    if (keyword) params.keyword = keyword;
+    if (sortBy) params.sortBy = sortBy;
+    if (sortOrder) params.sortOrder = sortOrder;
+    if (filters) params.filters = filters;
+
+    const response = await apiClient.get<PaginatedResponse<Area>>('/api/v1/admin/areas', params);
+
+    const areas = response.data || [];
+    const total = response.pagination?.total || 0;
+
+    const structuredOutput = {
+      total,
+      count: areas.length,
+      current: page || 1,
+      pageSize: pageSize || 20,
+      areas: areas.map((a: Area) => ({
+        id: a.id,
+        name: a.name,
+        code: a.code,
+        level: a.level,
+        parentId: a.parentId,
+        status: a.status,
+        createdAt: a.createdAt
+      })),
+      hasMore: total > (page || 1) * (pageSize || 20)
+    };
+
+    let textContent: string;
+    if (response_format === ResponseFormat.JSON) {
+      textContent = JSON.stringify(structuredOutput, null, 2);
+    } else {
+      let markdown = formatAreaListMarkdown(areas, total, page || 1, pageSize || 20);
+
+      // Check character limit
+      if (markdown.length > CHARACTER_LIMIT) {
+        const halfLength = Math.floor(areas.length / 2);
+        const truncatedAreas = areas.slice(0, halfLength);
+        markdown = formatAreaListMarkdown(truncatedAreas, total, page || 1, pageSize || 20);
+        markdown += `\n---\n⚠️ **Response truncated**: Showing ${halfLength} of ${areas.length} results. Use pagination to see more.\n`;
+      }
+
+      textContent = markdown;
+    }
+
+    return {
+      content: [{ type: 'text', text: textContent }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to list areas'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Get single area tool
+ */
+export const areaGetTool = async (args: AreaGetInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { id } = args;
+    const response = await apiClient.get<Area>(`/api/v1/admin/areas/${id}`);
+
+    const area = response;
+
+    const structuredOutput = {
+      id: area.id,
+      name: area.name,
+      code: area.code,
+      level: area.level,
+      parentId: area.parentId,
+      status: area.status,
+      createdAt: area.createdAt,
+      updatedAt: area.updatedAt
+    };
+
+    const markdown = formatAreaMarkdown(area);
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to get area'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Get area tree tool (returns hierarchical tree structure)
+ */
+export const areaGetTreeTool = async () => {
+  const apiClient = getApiClient();
+
+  try {
+    // Backend returns { success: true, data: Area[] }
+    const response = await apiClient.get<{ success: boolean; data: Area[] }>('/api/v1/admin/areas/tree');
+
+    // Handle both direct array and wrapped response formats
+    const areas = Array.isArray(response) ? response : (response?.data || []);
+
+    const structuredOutput = {
+      total: areas.length,
+      areas: areas.map((a: Area) => ({
+        id: a.id,
+        name: a.name,
+        code: a.code,
+        level: a.level,
+        parentId: a.parentId,
+        status: a.status ?? a.isDisabled ?? 1, // Handle both status and isDisabled fields
+        children: a.children?.map((c: Area) => ({
+          id: c.id,
+          name: c.name,
+          code: c.code,
+          level: c.level,
+          parentId: c.parentId,
+          status: c.status ?? c.isDisabled ?? 1
+        }))
+      }))
+    };
+
+    const markdown = [
+      '# Area Tree',
+      '',
+      `Total root areas: ${areas.length}`,
+      '',
+      formatAreaTreeMarkdown(areas)
+    ].join('\n');
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to get area tree'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Create area tool
+ */
+export const areaCreateTool = async (args: AreaCreateInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const response = await apiClient.post<Area>('/api/v1/admin/areas', args);
+
+    const area = response;
+
+    const structuredOutput = {
+      id: area.id,
+      name: area.name,
+      code: area.code,
+      level: area.level,
+      parentId: area.parentId,
+      status: area.status,
+      createdAt: area.createdAt
+    };
+
+    const markdown = `✅ **Area Created Successfully**\n\n${formatAreaMarkdown(area)}`;
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to create area'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Update area tool
+ */
+export const areaUpdateTool = async (args: AreaUpdateInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { id, ...updateData } = args;
+    const response = await apiClient.put<Area>(`/api/v1/admin/areas/${id}`, updateData);
+
+    const area = response;
+
+    const structuredOutput = {
+      id: area.id,
+      name: area.name,
+      code: area.code,
+      level: area.level,
+      parentId: area.parentId,
+      status: area.status,
+      updatedAt: area.updatedAt
+    };
+
+    const markdown = `✅ **Area Updated Successfully**\n\n${formatAreaMarkdown(area)}`;
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to update area'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Delete area tool
+ */
+export const areaDeleteTool = async (args: AreaDeleteInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { id } = args;
+    await apiClient.delete<{ success: boolean }>(`/api/v1/admin/areas/${id}`);
+
+    const markdown = `✅ **Area Deleted Successfully**\n\nArea ID ${id} has been deleted.`;
+
+    const structuredOutput = {
+      success: true,
+      deletedAreaId: id
+    };
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to delete area'}`
+      }]
+    };
+  }
+};
+
+// Export tool registration configs
+export const areaTools = {
+  areaList: {
+    name: 'admin_list_areas',
+    schema: AreaListInputSchema,
+    handler: areaListTool
+  },
+  areaGet: {
+    name: 'admin_get_area',
+    schema: AreaGetInputSchema,
+    handler: areaGetTool
+  },
+  areaGetTree: {
+    name: 'admin_get_area_tree',
+    schema: undefined, // No input schema needed
+    handler: areaGetTreeTool
+  },
+  areaCreate: {
+    name: 'admin_create_area',
+    schema: AreaCreateInputSchema,
+    handler: areaCreateTool
+  },
+  areaUpdate: {
+    name: 'admin_update_area',
+    schema: AreaUpdateInputSchema,
+    handler: areaUpdateTool
+  },
+  areaDelete: {
+    name: 'admin_delete_area',
+    schema: AreaDeleteInputSchema,
+    handler: areaDeleteTool
+  }
+};

+ 318 - 0
packages/admin-mcp-server/src/tools/company-tools.ts

@@ -0,0 +1,318 @@
+/**
+ * Company Management Tools
+ *
+ * MCP tools for managing companies in the admin system.
+ */
+
+import { getApiClient } from '../services/api-client.js';
+import {
+  CompanyListInputSchema,
+  CompanyGetInputSchema,
+  CompanyCreateInputSchema,
+  CompanyUpdateInputSchema,
+  CompanyDeleteInputSchema
+} from '../schemas/index.js';
+import type {
+  CompanyListInput,
+  CompanyGetInput,
+  CompanyCreateInput,
+  CompanyUpdateInput,
+  CompanyDeleteInput
+} from '../schemas/index.js';
+import { CHARACTER_LIMIT, ResponseFormat } from '../constants.js';
+import type { Company, PaginatedResponse } from '../types.js';
+
+/**
+ * Format company data for markdown output
+ */
+function formatCompanyMarkdown(company: Company): string {
+  const lines = [
+    `## Company: ${company.companyName} (ID: ${company.id})`,
+    '',
+    `**Company Name**: ${company.companyName}`,
+    company.contactPerson ? `**Contact Person**: ${company.contactPerson}` : null,
+    company.contactPhone ? `**Contact Phone**: ${company.contactPhone}` : null,
+    company.contactEmail ? `**Contact Email**: ${company.contactEmail}` : null,
+    company.address ? `**Address**: ${company.address}` : null,
+    company.platform ? `**Platform**: ${company.platform.name} (ID: ${company.platformId})` : null,
+    `**Status**: ${company.status === 1 ? 'Enabled' : 'Disabled'}`,
+    `**Created At**: ${new Date(company.createTime).toLocaleString('zh-CN')}`,
+    ''
+  ].filter(Boolean);
+
+  return lines.join('\n');
+}
+
+/**
+ * Format company list for markdown output
+ */
+function formatCompanyListMarkdown(companies: Company[], total: number, current: number, pageSize: number): string {
+  const lines = [
+    `# Company List`,
+    '',
+    `Total: ${total} companies | Page: ${current} | Page Size: ${pageSize}`,
+    ''
+  ];
+
+  for (const company of companies) {
+    lines.push(formatCompanyMarkdown(company));
+  }
+
+  return lines.join('\n');
+}
+
+/**
+ * List companies tool
+ */
+export const companyListTool = async (args: CompanyListInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { page, pageSize, keyword, sortBy, sortOrder, filters, response_format } = args;
+
+    const params: Record<string, unknown> = {
+      page: page || 1,
+      pageSize: pageSize || 20
+    };
+
+    if (keyword) params.keyword = keyword;
+    if (sortBy) params.sortBy = sortBy;
+    if (sortOrder) params.sortOrder = sortOrder;
+    if (filters) params.filters = filters;
+
+    const response = await apiClient.get<PaginatedResponse<Company>>('/api/v1/company', params);
+
+    const companies = response.data || [];
+    const total = response.pagination?.total || 0;
+
+    const structuredOutput = {
+      total,
+      count: companies.length,
+      current: page || 1,
+      pageSize: pageSize || 20,
+      companies: companies.map((c: Company) => ({
+        id: c.id,
+        companyName: c.companyName,
+        contactPerson: c.contactPerson,
+        contactPhone: c.contactPhone,
+        contactEmail: c.contactEmail,
+        address: c.address,
+        platformId: c.platformId,
+        platformName: c.platform?.name,
+        status: c.status,
+        createTime: c.createTime
+      })),
+      hasMore: total > (page || 1) * (pageSize || 20)
+    };
+
+    let textContent: string;
+    if (response_format === ResponseFormat.JSON) {
+      textContent = JSON.stringify(structuredOutput, null, 2);
+    } else {
+      let markdown = formatCompanyListMarkdown(companies, total, page || 1, pageSize || 20);
+
+      // Check character limit
+      if (markdown.length > CHARACTER_LIMIT) {
+        const halfLength = Math.floor(companies.length / 2);
+        const truncatedCompanies = companies.slice(0, halfLength);
+        markdown = formatCompanyListMarkdown(truncatedCompanies, total, page || 1, pageSize || 20);
+        markdown += `\n---\n⚠️ **Response truncated**: Showing ${halfLength} of ${companies.length} results. Use pagination to see more.\n`;
+      }
+
+      textContent = markdown;
+    }
+
+    return {
+      content: [{ type: 'text', text: textContent }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to list companies'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Get single company tool
+ */
+export const companyGetTool = async (args: CompanyGetInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { id } = args;
+    const response = await apiClient.get<Company>(`/api/v1/company/${id}`);
+
+    const company = response;
+
+    const structuredOutput = {
+      id: company.id,
+      companyName: company.companyName,
+      contactPerson: company.contactPerson,
+      contactPhone: company.contactPhone,
+      contactEmail: company.contactEmail,
+      address: company.address,
+      platformId: company.platformId,
+      platformName: company.platform?.name,
+      status: company.status,
+      createTime: company.createTime,
+      updateTime: company.updateTime
+    };
+
+    const markdown = formatCompanyMarkdown(company);
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to get company'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Create company tool
+ */
+export const companyCreateTool = async (args: CompanyCreateInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const response = await apiClient.post<Company>('/api/v1/company', args);
+
+    const company = response;
+
+    const structuredOutput = {
+      id: company.id,
+      companyName: company.companyName,
+      contactPerson: company.contactPerson,
+      contactPhone: company.contactPhone,
+      contactEmail: company.contactEmail,
+      address: company.address,
+      platformId: company.platformId,
+      status: company.status,
+      createTime: company.createTime
+    };
+
+    const markdown = `✅ **Company Created Successfully**\n\n${formatCompanyMarkdown(company)}`;
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to create company'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Update company tool
+ */
+export const companyUpdateTool = async (args: CompanyUpdateInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { id, ...updateData } = args;
+    const response = await apiClient.put<Company>(`/api/v1/company/${id}`, updateData);
+
+    const company = response;
+
+    const structuredOutput = {
+      id: company.id,
+      companyName: company.companyName,
+      contactPerson: company.contactPerson,
+      contactPhone: company.contactPhone,
+      contactEmail: company.contactEmail,
+      address: company.address,
+      platformId: company.platformId,
+      status: company.status,
+      updateTime: company.updateTime
+    };
+
+    const markdown = `✅ **Company Updated Successfully**\n\n${formatCompanyMarkdown(company)}`;
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to update company'}`
+      }]
+    };
+  }
+};
+
+/**
+ * Delete company tool
+ */
+export const companyDeleteTool = async (args: CompanyDeleteInput) => {
+  const apiClient = getApiClient();
+
+  try {
+    const { id } = args;
+    await apiClient.delete<{ success: boolean }>(`/api/v1/company/${id}`);
+
+    const markdown = `✅ **Company Deleted Successfully**\n\nCompany ID ${id} has been deleted.`;
+
+    const structuredOutput = {
+      success: true,
+      deletedCompanyId: id
+    };
+
+    return {
+      content: [{ type: 'text', text: markdown }],
+      structuredContent: structuredOutput
+    };
+  } catch (error) {
+    return {
+      content: [{
+        type: 'text',
+        text: `Error: ${error instanceof Error ? error.message : 'Failed to delete company'}`
+      }]
+    };
+  }
+};
+
+// Export tool registration configs
+export const companyTools = {
+  companyList: {
+    name: 'admin_list_companies',
+    schema: CompanyListInputSchema,
+    handler: companyListTool
+  },
+  companyGet: {
+    name: 'admin_get_company',
+    schema: CompanyGetInputSchema,
+    handler: companyGetTool
+  },
+  companyCreate: {
+    name: 'admin_create_company',
+    schema: CompanyCreateInputSchema,
+    handler: companyCreateTool
+  },
+  companyUpdate: {
+    name: 'admin_update_company',
+    schema: CompanyUpdateInputSchema,
+    handler: companyUpdateTool
+  },
+  companyDelete: {
+    name: 'admin_delete_company',
+    schema: CompanyDeleteInputSchema,
+    handler: companyDeleteTool
+  }
+};

+ 37 - 0
packages/admin-mcp-server/src/types.ts

@@ -177,6 +177,26 @@ export interface UpdateSystemConfigDto {
   category?: string;
 }
 
+// ============================================================================
+// Area Types
+// ============================================================================
+
+/**
+ * Area entity (geographic regions: provinces, cities, districts)
+ */
+export interface Area {
+  id: number;
+  name: string;
+  code: string;
+  level: number; // 1=province, 2=city, 3=district
+  parentId?: number | null;
+  status?: number;
+  isDisabled?: number; // Backend uses isDisabled in tree endpoint
+  children?: Area[];
+  createdAt: Date;
+  updatedAt: Date;
+}
+
 // ============================================================================
 // Company Types
 // ============================================================================
@@ -191,11 +211,28 @@ export interface Company {
   contactPhone?: string | null;
   contactEmail?: string | null;
   address?: string | null;
+  platformId?: number | null;
+  platform?: Platform;
   status: number;
   createTime: Date;
   updateTime: Date;
 }
 
+// ============================================================================
+// Platform Types
+// ============================================================================
+
+/**
+ * Platform entity
+ */
+export interface Platform {
+  id: number;
+  name: string;
+  status: number;
+  createdAt: Date;
+  updatedAt: Date;
+}
+
 // ============================================================================
 // File Types
 // ============================================================================