前端的AI路其之三:用MCP做一个日程助理

前言

话不多说,先演示一下吧。大概功能描述就是,告诉AI"添加日历,今天下午五点到六点,我要去万达吃饭",然后AI自动将日程同步到日历

准备工作

开发这个日程助理需要用到MCPMac(mac的日历能力)Windsurf(运行mcp)。技术栈是Typescript

思路

基于MCP我们可以做很多。关于这个日程助理,其实也是很简单一个尝试,其实就是再验证一下我对MCP的使用。因为Siri的原因,让我刚好有了这个想法,尝试一下自己搞个日程助理。关于MCP可以看我前面的分享 # 前端的AI路其之一: MCP与Function Calling# 前端的AI路其之二:初试MCP Server

我的思路如下: 让大模型理解一下我的意图,然后执行相关操作。这也是我对MCP的理解(执行相关操作)。因此要做日程助理,那就很简单了。首先搞一个脚本,能够自动调用mac并添加日历,然后再包装成MCP,最后引入大模型就ok了。顺着这个思路,接下来就讲讲如何实现吧

实现

第一步:在mac上添加日历

这里我们需要先明确一个概念。mac上给日历添加日程,其实是就是给对应的日历类型添加日程。举个例子

左边红框其实就是日历类型,比如我要添加一个开发日程,其实就是先选择"开发"日历,然后在该日历下添加日程。因此如果我们想通过脚本形式创建日程,其实就是先看日历类型存在不存在,如果存在,就在该类型下添加一个日程。

因此这里第一步,我们先获取mac上有没有对应的日历,没有的话就创建一个。

1.1 查找日历

参考文档mac查找日历

假定我们的日历类型叫做 日程助手这里我使用了applescript的语法,因为JavaScript的方式我这运行有问题。

js 复制代码
import { execSync } from 'child_process';

function checkCalendarExists(calendarName) {

    const   Script = `tell application "Calendar"
	set theCalendarName to "${calendarName}"
	set theCalendar to first calendar where its name = theCalendarName
end tell`;


  // 执行并解析结果
  try {
    const result = execSync(`osascript  -e '${Script}'`, { 
      encoding: 'utf-8',
      stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
    });

    console.log(result);
    return true;
  } catch (error) {
    console.error('检测失败:', error.message);
    return false;
  }
}

// 使用示例
const calendarName = '日程助手';
const exists = checkCalendarExists(calendarName);
console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');
附赠检验结果

现在我们知道了怎么判断日历存不存在,那么接下来就是,在日历不存在的时候创建日历

1.2 日历创建

参考文档 mac 创建日历

js 复制代码
import { execSync } from 'child_process';


// 创建日历
function  createCalendar(calendarName) {
    const script = `tell application "Calendar"
        make new calendar with properties {name:"${calendarName}"}
    end tell`;

    try {

        execSync(`osascript -e '${script}'`, { 
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
        });

        return true;
    } catch (e) {
        console.log('create fail', e)
        return false;
    }
}

// 检查日历是否存在
function checkCalendarExists(calendarName) {
    ....
}

// 使用示例
const calendarName = '日程助手';
const exists = checkCalendarExists(calendarName);
console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');

if (!exists) {
  const res =    createCalendar(calendarName);

  console.log(res ? '✅ 创建成功' : '❌ 创建失败')
}
运行结果

接下来就是第三步了,在日历"日程助手"下创建日程

1.3 创建日程

js 复制代码
import { execSync } from 'child_process';

// 创建日程
function createCalendarEvent(calendarName, config) {

    const script = `var app = Application.currentApplication()
    app.includeStandardAdditions = true
    var Calendar = Application("Calendar")
     
    var eventStart = new Date(${config.startTime})
    var eventEnd = new Date(${config.endTime})
     
    var projectCalendars = Calendar.calendars.whose({name: "${calendarName}"})
    var projectCalendar = projectCalendars[0]
    var event = Calendar.Event({summary: "${config.title}", startDate: eventStart, endDate: eventEnd, description: "${config.description}"})
    projectCalendar.events.push(event)
    event`

    try {
        console.log('开始创建日程');
        execSync(`  osascript -l JavaScript -e '${script}'`, { 
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
        });
        console.log('✅ 日程添加成功');
    } catch (error) {
        console.error('❌ 执行失败:', error);
    }

}

// 创建日历
function  createCalendar(calendarName) {
   ....
}

// 检查日历是否存在
function checkCalendarExists(calendarName) {

 ...
}

这里我们完善一下代码

js 复制代码
import { execSync } from 'child_process';

function handleCreateEvent(config) {
    const calendarName = '日程助手';
    const exists = checkCalendarExists(calendarName);
    // console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');

    if (!exists) {
        const createRes =  createCalendar(calendarName);

        console.log(createRes ? '✅ 创建日历成功' : '❌ 创建日历失败')

        if (createRes) {
            createCalendarEvent(calendarName, config)
        }
    }  else {
        createCalendarEvent(calendarName, config)
    }
}

// 创建日程
function createCalendarEvent(calendarName, config) {

    const script = `var app = Application.currentApplication()
    app.includeStandardAdditions = true
    var Calendar = Application("Calendar")
     
    var eventStart = new Date(${config.startTime})
    var eventEnd = new Date(${config.endTime})
     
    var projectCalendars = Calendar.calendars.whose({name: "${calendarName}"})
    var projectCalendar = projectCalendars[0]
    var event = Calendar.Event({summary: "${config.title}", startDate: eventStart, endDate: eventEnd, description: "${config.description}"})
    projectCalendar.events.push(event)
    event`

    try {
        console.log('开始创建日程');
        execSync(`  osascript -l JavaScript -e '${script}'`, { 
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
        });
        console.log('✅ 日程添加成功');
    } catch (error) {
        console.error('❌ 执行失败:', error);
    }

}

// 创建日历
function  createCalendar(calendarName) {
    const script = `tell application "Calendar"
        make new calendar with properties {name:"${calendarName}"}
    end tell`;

    try {

        execSync(`osascript -e '${script}'`, { 
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
        });

        return true;
    } catch (e) {
        console.log('create fail', e)
        return false;
    }
}

// 检查日历是否存在
function checkCalendarExists(calendarName) {

    const   Script = `tell application "Calendar"
	set theCalendarName to "${calendarName}"
	set theCalendar to first calendar where its name = theCalendarName
end tell`;


  // 执行并解析结果
  try {
    const result = execSync(`osascript  -e '${Script}'`, { 
      encoding: 'utf-8',
      stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
    });

    return true;
  } catch (error) {
    return false;
  }
}


// 运行示例

const eventConfig = {
    title: '团队周会',
    startTime: 1744183538021,
    endTime: 1744442738000,
    description: '每周项目进度同步',
};

handleCreateEvent(eventConfig)
运行结果

这就是一个完善的,可以直接在终端运行的创建日程的脚本的。接下来我们要做的就是,让大模型理解这个脚本,并学会使用这个脚本

第二步: 定义MCP

基于第一步,我们已经完成了这个日程助理的基本功能,接下来就是借助MCP的能力,教会大模型知道有这个函数,以及怎么调用这个函数

js 复制代码
// 引入 mcp
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 声明MCP服务
const server = new McpServer({
    name: "mcp_calendar",
    version: "1.0.0"
  });

...
// 添加日历函数 也就是告诉大模型 有这个东西以及怎么用
server.tool("add_mac_calendar", '给mac日历添加日程, 接受四个参数  startTime, endTime是起止时间(格式为YYYY-MM-DD HH:MM:SS) title是日历标题  description是日历描述', { startTime: z.string(), endTime: z.string(), title: z.string(), description: z.string() },
async ({ startTime, endTime, title, description }) => {
    const res =  handleCreateEvent({
        title: title,
        description: description,
        startTime: new Date(startTime).getTime(),
        endTime: new Date(endTime).getTime()
      });
      return {
        content: [{ type: "text", text: res ? '添加成功' : '添加失败' }]
      }
})


// 初始化服务
const transport = new StdioServerTransport();
await server.connect(transport);

这里附上完整的ts代码

js 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { execSync } from 'child_process';
import { z } from "zod";


export interface EventConfig {
    // 日程标题
  title: string;
  // 日程开始时间 毫秒时间戳
  startTime: number;
  // 日程结束时间 毫秒时间戳
  endTime: number;
//   日程描述
  description: string;
}

const server = new McpServer({
    name: "mcp_calendar",
    version: "1.0.0"
  });

function handleCreateEvent(config: EventConfig) {
    const calendarName = '日程助手';
    const exists = checkCalendarExists(calendarName);
    // console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');

    let res = false;

    if (!exists) {
        const createRes =  createCalendar(calendarName);

        console.log(createRes ? '✅ 创建日历成功' : '❌ 创建日历失败')

        if (createRes) {
           res = createCalendarEvent(calendarName, config)
        }
    }  else {
        res = createCalendarEvent(calendarName, config)
    }

    return res
}

// 创建日程
function createCalendarEvent(calendarName: string, config: EventConfig) {

    const script = `var app = Application.currentApplication()
    app.includeStandardAdditions = true
    var Calendar = Application("Calendar")
     
    var eventStart = new Date(${config.startTime})
    var eventEnd = new Date(${config.endTime})
     
    var projectCalendars = Calendar.calendars.whose({name: "${calendarName}"})
    var projectCalendar = projectCalendars[0]
    var event = Calendar.Event({summary: "${config.title}", startDate: eventStart, endDate: eventEnd, description: "${config.description}"})
    projectCalendar.events.push(event)
    event`

    try {
        console.log('开始创建日程');
        execSync(`  osascript -l JavaScript -e '${script}'`, { 
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
        });
        console.log('✅ 日程添加成功');

        return true
    } catch (error) {
        console.error('❌ 执行失败:', error);
        return false
    }

}

// 创建日历
function  createCalendar(calendarName: string) {
    const script = `tell application "Calendar"
        make new calendar with properties {name:"${calendarName}"}
    end tell`;

    try {

        execSync(`osascript -e '${script}'`, { 
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
        });

        return true;
    } catch (e) {
        console.log('create fail', e)
        return false;
    }
}

// 检查日历是否存在
function checkCalendarExists(calendarName: string) {

    const   Script = `tell application "Calendar"
	set theCalendarName to "${calendarName}"
	set theCalendar to first calendar where its name = theCalendarName
end tell`;


  // 执行并解析结果
  try {
    const result = execSync(`osascript  -e '${Script}'`, { 
      encoding: 'utf-8',
      stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
    });

    return true;
  } catch (error) {
    return false;
  }
}


server.tool("add_mac_calendar", '给mac日历添加日程, 接受四个参数  startTime, endTime是起止时间(格式为YYYY-MM-DD HH:MM:SS) title是日历标题  description是日历描述', { startTime: z.string(), endTime: z.string(), title: z.string(), description: z.string() },
async ({ startTime, endTime, title, description }) => {
    const res =  handleCreateEvent({
        title: title,
        description: description,
        startTime: new Date(startTime).getTime(),
        endTime: new Date(endTime).getTime()
      });
      return {
        content: [{ type: "text", text: res ? '添加成功' : '添加失败' }]
      }
})

const transport = new StdioServerTransport();
await server.connect(transport);

第三步: 导入Windsurf

在前文已经讲过如何引入到Windsurf,可以参考前文# 前端的AI路其之二:初试MCP Server,这里就不过多赘述了。 其实在build之后,完全可以引入其他支持MCP的软件基本都是可以的。

接下来就是愉快的调用时间啦。

总结

这里其实是对前文# 前端的AI路其之二:初试MCP Server的再次深入。算是大概讲明白了Tool方式怎么用,MCP当然不止这一种用法,后面也会继续输出自己的学习感悟,也欢迎各位大佬的分享和指正。

祝好。

相关推荐
独立开阀者_FwtCoder12 分钟前
# 一天 Star 破万的开源项目「GitHub 热点速览」
前端·javascript·面试
天天扭码23 分钟前
前端进阶 | 面试必考—— JavaScript手写定时器
前端·javascript·面试
梦雨生生39 分钟前
拖拉拽效果加点击事件
前端·javascript·css
前端Hardy1 小时前
第2课:变量与数据类型——JS的“记忆盒子”
前端·javascript
冴羽1 小时前
SvelteKit 最新中文文档教程(23)—— CLI 使用指南
前端·javascript·svelte
jstart千语1 小时前
【SpringBoot】HttpServletRequest获取使用及失效问题(包含@Async异步执行方案)
java·前端·spring boot·后端·spring
徐小夕1 小时前
花了2个月时间,写了一款3D可视化编辑器3D-Tony
前端·javascript·react.js
凕雨1 小时前
Cesium学习笔记——dem/tif地形的分块与加载
前端·javascript·笔记·学习·arcgis·vue
程序猿小玉兒1 小时前
若依框架免登陆、页面全屏显示、打开新标签页(看板大屏)
前端
TGITCIC2 小时前
智驱未来:AI大模型重构数据治理新范式
大模型·数据治理·ai agent·rag检索增强·mcp·大模型数据·ai数据