diff --git a/mateclaw-server/src/main/java/vip/mate/channel/feishu/tool/FeishuChannelToolProvider.java b/mateclaw-server/src/main/java/vip/mate/channel/feishu/tool/FeishuChannelToolProvider.java
new file mode 100644
index 00000000..8a75d88c
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/channel/feishu/tool/FeishuChannelToolProvider.java
@@ -0,0 +1,211 @@
+package vip.mate.channel.feishu.tool;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.lark.oapi.Client;
+import com.lark.oapi.service.calendar.v4.model.CalendarEvent;
+import com.lark.oapi.service.calendar.v4.model.ListCalendarEventReq;
+import com.lark.oapi.service.calendar.v4.model.ListCalendarEventResp;
+import com.lark.oapi.service.docx.v1.model.CreateDocumentReq;
+import com.lark.oapi.service.docx.v1.model.CreateDocumentReqBody;
+import com.lark.oapi.service.docx.v1.model.CreateDocumentResp;
+import com.lark.oapi.service.docx.v1.model.RawContentDocumentReq;
+import com.lark.oapi.service.docx.v1.model.RawContentDocumentResp;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.stereotype.Component;
+import vip.mate.channel.feishu.FeishuClientFactory;
+import vip.mate.channel.tool.ChannelToolCallback;
+import vip.mate.channel.tool.ChannelToolContext;
+import vip.mate.channel.tool.ChannelToolDescriptor;
+import vip.mate.channel.tool.ChannelToolProvider;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * First concrete {@link ChannelToolProvider} — exposes a representative
+ * subset of Feishu's OpenAPI as Agent tools:
+ *
+ *
+ * - {@code feishu_calendar_list_events} (read) —
+ * {@code calendar/v4 calendarEvent.list}
+ * - {@code feishu_doc_read} (read) —
+ * {@code docx/v1 document.rawContent}
+ * - {@code feishu_doc_create} (write) —
+ * {@code docx/v1 document.create}
+ *
+ *
+ * Both reads ship default-enabled; the write ships default-disabled
+ * and {@code ChannelToolService} seeds a HIGH-severity guard rule so a
+ * call falls into {@code NEEDS_APPROVAL} via {@code DbRuleGuardian}.
+ *
+ *
Tool I/O uses JSON — input is the tool's argument JSON string,
+ * output is a compact JSON result (success or {@code "error"} key).
+ * Returning a structured object rather than free text keeps the LLM
+ * downstream branch-friendly.
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class FeishuChannelToolProvider implements ChannelToolProvider {
+
+ private final FeishuClientFactory clientFactory;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public String channelType() {
+ return "feishu";
+ }
+
+ @Override
+ public List describeTools() {
+ return FeishuToolCatalog.descriptors();
+ }
+
+ @Override
+ public List createTools(ChannelToolContext context) {
+ Long channelId = context.channelId();
+ List out = new ArrayList<>(3);
+
+ for (ChannelToolDescriptor d : FeishuToolCatalog.descriptors()) {
+ out.add(new ChannelToolCallback(
+ d.name(), d.description(), d.inputSchema(),
+ input -> dispatch(d.name(), channelId, input)));
+ }
+ return out;
+ }
+
+ /** Route by tool name. Kept in one place so the descriptor catalog drives the surface. */
+ private String dispatch(String toolName, Long channelId, String input) {
+ try {
+ Client client = clientFactory.client(channelId);
+ return switch (toolName) {
+ case FeishuToolCatalog.TOOL_LIST_EVENTS -> handleListEvents(client, input);
+ case FeishuToolCatalog.TOOL_DOC_READ -> handleDocRead(client, input);
+ case FeishuToolCatalog.TOOL_DOC_CREATE -> handleDocCreate(client, input);
+ default -> errorJson("Unknown Feishu tool: " + toolName);
+ };
+ } catch (Exception e) {
+ log.warn("[feishu-tool] {} failed: {}", toolName, e.getMessage());
+ return errorJson(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // Handlers
+ // ------------------------------------------------------------------
+
+ private String handleListEvents(Client client, String input) throws Exception {
+ JsonNode args = objectMapper.readTree(input == null || input.isBlank() ? "{}" : input);
+ String calendarId = textArg(args, "calendar_id");
+ if (calendarId == null) return errorJson("calendar_id is required");
+
+ ListCalendarEventReq.Builder req = ListCalendarEventReq.newBuilder().calendarId(calendarId);
+ String startTime = textArg(args, "start_time");
+ String endTime = textArg(args, "end_time");
+ Integer pageSize = intArg(args, "page_size");
+ if (startTime != null) req.startTime(startTime);
+ if (endTime != null) req.endTime(endTime);
+ if (pageSize != null) req.pageSize(pageSize);
+
+ ListCalendarEventResp resp = client.calendar().v4().calendarEvent().list(req.build());
+ if (!resp.success() || resp.getData() == null) {
+ return errorJson("calendar list failed: code=" + resp.getCode() + ", msg=" + resp.getMsg());
+ }
+ CalendarEvent[] items = resp.getData().getItems();
+ Map result = new LinkedHashMap<>();
+ result.put("count", items == null ? 0 : items.length);
+ // Compact representation — title + start + end keeps the LLM context tight.
+ List