UNPKG

@mfukushim/map-traveler-mcp

Version:
919 lines (918 loc) 43.5 kB
/*! map-traveler-mcp | MIT License | https://github.com/mfukushim/map-traveler-mcp */ import { Effect, Option } from "effect"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { defaultAvatarId, RunnerService, RunnerServiceLive, useAiImageGen } from "./RunnerService.js"; import { MapService, MapServiceLive } from "./MapService.js"; import { __pwd, DbService, DbServiceLive, env } from "./DbService.js"; import { StoryService, StoryServiceLive } from "./StoryService.js"; import { FetchHttpClient, HttpClient } from "@effect/platform"; import { defaultBaseCharPrompt, ImageService, ImageServiceLive } from "./ImageService.js"; import { logSync, McpLogService, McpLogServiceLive } from "./McpLogService.js"; import { AnswerError } from "./mapTraveler.js"; import { SnsService, SnsServiceLive } from "./SnsService.js"; import * as path from "path"; import * as fs from "node:fs"; import dayjs from "dayjs"; import { bodyAreaRatio, bodyHWRatio, bodyWindowRatioH, bodyWindowRatioW, bs_handle, noImageOut } from "./EnvUtils.js"; const feedTag = "#geo_less_traveler"; const feedUri = "at://did:plc:ygcsenazbvhyjmxeltz4fgw4/app.bsky.feed.generator/marble_square25"; const LabelGoogleMap = 'Google Map'; const LabelClaude = 'Claude'; const labelImage = (aiGen) => { return aiGen === 'pixAi' ? 'PixAi' : aiGen === 'sd' ? 'Stability.ai' : ''; }; const server = new Server({ name: "geo-less-traveler", version: "0.1.0", }, { capabilities: { resources: { listChanged: true }, tools: { listChanged: true }, prompts: {}, sampling: {}, }, }); export function sendProgressNotification(progressToken, total, progress) { return Effect.tryPromise({ try: () => server.notification({ method: "notifications/progress", params: { progress: progress, total: total, progressToken, }, }), catch: error => { logSync(`sendProgressNotification catch:${error}`); return new Error(`sendProgressNotification error:${error}`); } }).pipe(Effect.tap(() => logSync('do sendProgressNotification'))); } function sendToolListChanged() { logSync('start sendToolListChanged'); return Effect.tryPromise({ try: () => server.sendToolListChanged(), catch: error => { McpLogService.logError(`sendToolListChanged error:${error}`); return new Error(`sendToolListChanged error:${error}`); } }).pipe(Effect.tap(() => logSync('sendToolListChanged put'))); } function sendLoggingMessage(message, level = 'info') { return Effect.tryPromise({ try: () => server.sendLoggingMessage({ level: level, data: message, }), catch: error => { McpLogService.logError(`sendLoggingMessage catch:${error}`); return new Error(`sendLoggingMessage error:${error}`); } }); } export class McpService extends Effect.Service()("traveler/McpService", { accessors: true, effect: Effect.gen(function* () { const SETTING_COMMANDS = [ { name: "tips", description: "Inform you of recommended actions for your device", inputSchema: { type: "object", properties: {} } }, { name: "get_setting", description: "Get current setting", inputSchema: { type: "object", properties: {} } }, { name: "get_traveler_info", description: "get a traveler's setting.For example, traveler's name, the language traveler speak, Personality and speaking habits, etc.", inputSchema: { type: "object", properties: { settings: {}, } } }, { name: "set_traveler_info", description: "set a traveler's setting.For example, traveler's name, the language traveler speak, Personality and speaking habits, etc.", inputSchema: { type: "object", properties: { settings: { type: "string", description: "traveler's setting. traveler's name, the language traveler speak, etc." }, }, required: ["settings"] } }, ]; const AVATAR_PROMPT_COMMANDS = [ { name: "set_avatar_prompt", description: "set a traveler's avatar prompt. A prompt for AI image generation to specify the appearance of a traveler's avatar", inputSchema: { type: "object", properties: { prompt: { type: "string", description: "traveler's avatar AI image generation prompt." }, }, required: ["prompt"] } }, { name: "reset_avatar_prompt", description: "reset to default traveler's avatar prompt.", inputSchema: { type: "object", properties: {}, } }, ]; const START_STOP_COMMAND = [ { name: env.personMode === 'second' ? "start_journey" : "start_traveler_journey", description: env.personMode === 'second' ? "Start the journey to destination" : "Start the traveler's journey to destination", inputSchema: { type: "object", properties: {}, } }, { name: env.personMode === 'second' ? "stop_journey" : "stop_traveler_journey", description: env.personMode === 'second' ? "Stop the journey" : "Stop the traveler's journey", inputSchema: { type: "object", properties: {}, } }, ]; const SKIP_COMMAND = [ { name: "reach_a_percentage_of_destination", description: "Reach a specified percentage of the destination", inputSchema: { type: "object", properties: { timeElapsedPercentage: { type: "number", description: "Percent progress towards destination. (0~100)" }, }, required: ["timeElapsedPercentage"] } }, ]; const GET_VIEW_COMMAND = [ { name: env.personMode === 'second' ? "get_current_view_info" : "get_traveler_view_info", description: env.personMode === 'second' ? "Get the address of the current location and information on nearby facilities,view snapshot" : "Get the address of the current traveler's location and information on nearby facilities,view snapshot", inputSchema: { type: "object", properties: { includePhoto: { type: "boolean", description: "Get scenery photos of current location" }, includeNearbyFacilities: { type: "boolean", description: "Get information on nearby facilities" }, } } }, { name: "get_traveler_location", description: "Get the address of the current traveler's location", inputSchema: { type: "object", properties: {} } }, ]; const SNS_COMMAND = [ { name: "get_sns_mentions", description: "Get recent social media mentions", inputSchema: { type: "object", properties: {}, } }, { name: "get_sns_feeds", description: "Get recent social media posts from fellow travelers feeds", inputSchema: { type: "object", properties: {}, } }, { name: "post_sns_writer", description: "Post your recent travel experiences to social media for fellow travelers and readers.", inputSchema: { type: "object", properties: { message: { type: "string", description: "A description of the journey. important: Do not use offensive language." } }, required: ["message"] } }, { name: "reply_sns_writer", description: "Write a reply to the article with the specified ID.", inputSchema: { type: "object", properties: { message: { type: "string", description: "A description of the reply article. important: Do not use offensive language." }, id: { type: "string", description: "The ID of the original post to which you want to add a reply." } }, required: ["message", "id"] } }, { name: "add_like", description: "Add a like to the specified post", inputSchema: { type: "object", properties: { id: { type: "string", description: "The ID of the post to like." } }, required: ["id"] } }, ]; const makeToolsDef = () => { const def = () => { if (env.isPractice) { return [ ...GET_VIEW_COMMAND, ...SETTING_COMMANDS, ...START_STOP_COMMAND ]; } else { const basicToolsCommand = [ { name: env.personMode === 'second' ? "set_current_location" : "set_traveler_location", description: env.personMode === 'second' ? "Set my current address" : "Set the traveler's current address", inputSchema: { type: "object", properties: { address: { type: "string", description: env.personMode === 'second' ? "address to set" : "address set to traveler" } }, required: ["address"] } }, { name: env.personMode === 'second' ? "get_destination_address" : "get_traveler_destination_address", description: env.personMode === 'second' ? "get a address of destination location" : "get a address of traveler's destination location", inputSchema: { type: "object", properties: {} } }, { name: env.personMode === 'second' ? "set_destination_address" : "set_traveler_destination_address", description: env.personMode === 'second' ? "set a address of destination" : "set a address of traveler's destination", inputSchema: { type: "object", properties: { address: { type: "string", description: "address of destination" } }, required: ["address"] } }, ]; const cmd = []; if (env.travelerExist) { cmd.push(...basicToolsCommand, ...GET_VIEW_COMMAND, ...SETTING_COMMANDS); if (env.moveMode === "skip") { cmd.push(...SKIP_COMMAND); } else { cmd.push(...START_STOP_COMMAND); } if (env.anySnsExist) { cmd.push(...SNS_COMMAND); } if (!env.fixedModelPrompt) { cmd.push(...AVATAR_PROMPT_COMMANDS); } } else { cmd.push({ name: "call_traveler", description: "call the traveler", inputSchema: { type: "object", properties: {} } }); } return cmd; } }; return Effect.succeed(def()).pipe(Effect.andThen(a => ({ tools: env.filterTools.length > 0 ? a.filter(b => env.filterTools.includes(b.name)) : a }))); }; const tips = () => { return Effect.gen(function* () { const res = yield* StoryService.tips(); const content = [{ type: "text", text: res.textList.join('\n-------\n') }]; if (res.imagePathList.length > 0) { yield* Effect.forEach(res.imagePathList, a => { return Effect.async((resume) => fs.readFile(path.join(__pwd, a), (err, data) => { if (err) { resume(Effect.fail(err)); } resume(Effect.succeed(data)); })).pipe(Effect.andThen(b => { content.push({ type: "image", data: Buffer.from(b).toString('base64'), mimeType: 'image/png' }); })); }); } return content; }).pipe(Effect.provide(StoryServiceLive)); }; const setPersonMode = (person) => { const mode = person === 'second_person' ? 'second' : 'third'; env.personMode = mode; return DbService.saveEnv('personMode', mode).pipe(Effect.tap(() => sendToolListChanged()), Effect.andThen(a => [{ type: "text", text: `Person mode set as follows: ${a.value}` }])); }; const getTravelerInfo = () => { return DbService.getEnv('aiEnv').pipe(Effect.andThen(a => [{ type: "text", text: `The traveller's information is as follows: ${a}` }]), Effect.orElseSucceed(() => [{ type: "text", text: `There is no traveler information.` }])); }; const setTravelerInfo = (info) => { return DbService.saveEnv('aiEnv', info).pipe(Effect.andThen(a => [{ type: "text", text: `The traveller information is as follows: ${a.value}` }])); }; const getSetting = () => { return Effect.gen(function* () { const version = yield* DbService.getVersion(); const envText = 'A json of current environment settings\n' + Object.entries(env).map(([key, value]) => { return `${key}= ${JSON.stringify(value)}`; }).join('\n') + '\nList of Image settings\n' + (bodyAreaRatio ? `bodyAreaRatio=${bodyAreaRatio}\n` : '') + (bodyHWRatio ? `bodyHWRatio=${bodyHWRatio}\n` : '') + (bodyWindowRatioW ? `bodyWindowRatioW=${bodyWindowRatioW}\n` : '') + (bodyWindowRatioH ? `bodyWindowRatioH=${bodyWindowRatioH}\n` : '') + (noImageOut ? `noImage=true\n` : '') + `version=${version}\n`; return [{ type: "text", text: envText }]; }); }; const setAvatarPrompt = (prompt) => { return DbService.updateBasePrompt(defaultAvatarId, prompt).pipe(Effect.andThen(a => [{ type: "text", text: `Set traveller prompt to: "${a}"` }]), Effect.tap(() => { env.promptChanged = true; return DbService.saveEnv('promptChanged', '1'); })); }; const resetAvatarPrompt = () => { return DbService.updateBasePrompt(defaultAvatarId, defaultBaseCharPrompt).pipe(Effect.andThen(() => [{ type: "text", text: `reset traveller prompt to default.` }])); }; const getCurrentLocationInfo = (includePhoto, includeNearbyFacilities) => { return RunnerService.getCurrentView(dayjs(), includePhoto, includeNearbyFacilities, env.isPractice).pipe(Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive])); }; const getElapsedView = (timeElapsedPercentage) => { if (env.isPractice) { return practiceNotUsableMessage; } return RunnerService.getElapsedView(timeElapsedPercentage).pipe(Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive])); }; const practiceNotUsableMessage = Effect.succeed([ { type: "text", text: 'Sorry,this feature not usable in practice mode. ' + 'Please assign Google Map API key and restart application if you use this function. ' + 'For more information, please see the resource "SettingInfo"' } ]); const setCurrentLocation = (location) => { if (env.isPractice) { return practiceNotUsableMessage; } if (!location) { throw new AnswerError("Location address is required"); } return Effect.gen(function* () { const address = yield* MapService.getMapLocation(location); if (Option.isNone(address)) { return yield* Effect.fail(new AnswerError("I don't know where you're talking about. location not found")); } const timeZoneId = yield* MapService.getTimezoneByLatLng(address.value.lat, address.value.lng); yield* McpLogService.logTrace(address.value); yield* DbService.saveRunStatus({ id: 1, status: 'stop', from: '', to: address.value.address, destination: null, startLat: 0, startLng: 0, endLat: address.value.lat, endLng: address.value.lng, durationSec: 0, distanceM: 0, avatarId: 1, tripId: 0, tilEndEpoch: 0, startTime: new Date(0), endTime: new Date(0), startCountry: address.value.country, endCountry: address.value.country, startTz: timeZoneId, endTz: timeZoneId, currentPathNo: -1, currentStepNo: -1, }); const setMessage = [ `location set succeed`, `address:${address.value.address}`, `latitude:${address.value.lat}, longitude:${address.value.lng}` ]; yield* DbService.getEnv('destination').pipe(Effect.andThen(dest => RunnerService.setDestinationAddress(dest)), Effect.andThen(a => setMessage.push(a.message)), Effect.orElse(() => Effect.succeed(true))); return [{ type: "text", text: setMessage.join('\n') }]; }).pipe(Effect.provide([MapServiceLive, DbServiceLive, RunnerServiceLive])); }; const getDestinationAddress = () => { if (env.isPractice) { return practiceNotUsableMessage; } return RunnerService.getDestinationAddress().pipe(Effect.andThen(a => [{ type: "text", text: `Current destination is "${a}"` }]), Effect.provide([MapServiceLive, StoryServiceLive, RunnerServiceLive, ImageServiceLive, DbServiceLive])); }; const setDestinationAddress = (address) => { if (env.isPractice) { return practiceNotUsableMessage; } return RunnerService.setDestinationAddress(address).pipe(Effect.andThen(a => [{ type: "text", text: a.message }]), Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, ImageServiceLive])); }; const startJourney = () => { return RunnerService.startJourney(env.isPractice).pipe(Effect.andThen(a => { const out = [{ type: "text", text: a.text }]; if (Option.isSome(a.image)) { out.push({ type: "image", data: a.image.value.toString("base64"), mimeType: 'image/png' }); } return out; }), Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive])); }; const stopJourney = () => { return RunnerService.stopJourney(env.isPractice).pipe(Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive])); }; const setTravelerExist = (callKick) => { env.travelerExist = callKick; return McpLogService.log(`enter setTravelerExist:${callKick}`).pipe(Effect.tap(() => sendToolListChanged()), Effect.andThen(() => { return [{ type: "text", text: (callKick ? "traveler called" : "traveler kicked") }]; })); }; const makeVisitorMessage = (notification) => { return Effect.gen(function* () { const visitorProf = yield* SnsService.getProfile(notification.handle); const recentvisitorPosts = yield* SnsService.getAuthorFeed(notification.handle, 3); const mentionPostText = yield* SnsService.getPost(notification.uri).pipe(Effect.andThen(a => Effect.succeed(Option.fromNullable(a.posts.length > 0 ? a.posts[0].record.text : undefined)))); const repliedPostText = notification.mentionType === 'reply' && notification.parentUri ? (yield* SnsService.getPost(notification.parentUri).pipe(Effect.andThen(a => Effect.succeed(Option.fromNullable(a.posts.length > 0 ? a.posts[0].record.text : undefined))))) : Option.none(); const visitorName = notification.name || notification.handle || '誰か'; yield* McpLogService.logTrace(`avatarName:${visitorName}`); let recentVisitorPost = ''; let recentVisitorPostId = ''; if (recentvisitorPosts && recentvisitorPosts.feed.length > 0) { const p = recentvisitorPosts.feed[0].post; recentVisitorPost = p.record.text; recentVisitorPostId = p.uri + '-' + p.cid; } yield* McpLogService.logTrace(`mentionPostText:${Option.getOrUndefined(mentionPostText)}`); yield* McpLogService.logTrace(`repliedPostText:${Option.getOrUndefined(repliedPostText)}`); yield* McpLogService.logTrace(`visitorPostText:${recentVisitorPost}`); return { visitorName, recentVisitorPost: recentVisitorPost, visitorProf: visitorProf.description, mentionPost: mentionPostText, repliedPost: repliedPostText, target: notification.mentionType === 'reply' ? notification.uri + '-' + notification.cid : recentVisitorPost.includes(feedTag) ? recentVisitorPostId : notification.uri + '-' + notification.cid }; }); }; const getSnsMentions = () => { return Effect.gen(function* () { const notifications = yield* SnsService.getNotification(); const { reply, like } = notifications.reduce((p, c) => { if (c.mentionType === 'reply') { p.reply.push(c); } else if (c.mentionType === 'like') { p.like.push(c); } return p; }, { reply: [], like: [] }); const likeMes = yield* Effect.forEach(like, a => makeVisitorMessage(a)); const replyMes = yield* Effect.forEach(reply.filter(v => v.rootUri != v.parentUri), a => makeVisitorMessage(a)); const likeText = `Our SNS post received the following likes.\n` + `|id|Name of the person who liked the post|recent article by the person who liked this|Profile of the person who liked|\n` + likeMes.map((a) => `|"${a.target}"|${a.visitorName}|${a.recentVisitorPost}|${a.visitorProf}|`).join('\n') + '\n'; const replyText = `We received the following reply to our SNS post:\n` + `|id|The name of the person who replied|Content of the reply article|Profile of the person who replied|\n` + replyMes.map((a) => `|"${a.target}"|${a.visitorName}|${Option.getOrElse(a.mentionPost, () => '')}|${a.visitorProf || ''}|`).join('\n') + '\n'; const content = []; if (replyMes.length > 0) { content.push({ type: 'text', text: replyText }); } if (likeMes.length > 0) { content.push({ type: 'text', text: likeText }); } return content; }).pipe(Effect.provide(SnsServiceLive)); }; const readSnsReader = () => { return Effect.succeed([]); }; const getSnsFeeds = () => { return Effect.gen(function* () { const posts = yield* SnsService.getFeed(feedUri, 4); const detectFeeds = posts.filter(v => v.post.author.handle !== bs_handle) .reduce((p, c) => { if (!p.find(v => v.post.author.handle === c.post.author.handle)) { p.push(c); } return p; }, []); const select = detectFeeds.map(v => { const im = v.post.embed?.images; return ({ id: v.post.uri + '-' + v.post.cid, authorHandle: v.post.author.displayName || v.post.author.handle, body: v.post.record.text || '', imageUri: im ? im[0].thumb : undefined }); }); const out = select.map(v => `id: ${v.id} \nauthor: ${v.authorHandle}\nbody: ${v.body}`).join('\n-----\n'); const images = select.flatMap(v => v.imageUri ? [{ uri: v.imageUri, handle: v.authorHandle }] : []); const imageOut = yield* Effect.forEach(images, (a) => { return HttpClient.get(a.uri).pipe(Effect.andThen((response) => response.arrayBuffer), Effect.scoped, Effect.provide(FetchHttpClient.layer)).pipe(Effect.andThen(a1 => ({ type: "image", data: Buffer.from(a1).toString("base64"), mimeType: "image/jpeg" }))); }); const c = [{ type: 'text', text: `I got the following article:\n-----\n${out}\n-----\n` }]; c.push(...imageOut); return c; }).pipe(Effect.provide(SnsServiceLive)); }; const addLike = (id) => { return Effect.gen(function* () { if (env.noSnsPost) { const noMes = [ { type: "text", text: env.loggingMode ? "like to log." : "Like to SNS is stopped by env settings." } ]; if (env.loggingMode) { return yield* McpLogService.logTrace(`log like:${id}`).pipe(Effect.andThen(() => noMes)); } return yield* Effect.succeed(noMes); } const exec = /^"?(at.+?)"?$/.exec(id); const id2 = exec && exec[1]; if (!id2) { return yield* Effect.fail(new AnswerError('id is invalid')); } return yield* SnsService.addLike(id2).pipe(Effect.andThen(() => [ { type: "text", text: "Liked" } ]), Effect.provide(SnsServiceLive)); }); }; const replySnsWriter = (message, id) => { return Effect.gen(function* () { if (env.noSnsPost) { const noMes = [ { type: "text", text: env.loggingMode ? "posted to log." : "Posting to SNS is stopped by env settings." } ]; if (env.loggingMode) { return yield* McpLogService.log(message).pipe(Effect.andThen(() => noMes)); } return yield* Effect.succeed(noMes); } const exec = /^"?(at.+?)"?$/.exec(id); const id2 = exec && exec[1]; if (!id2) { return yield* Effect.fail(new AnswerError('id is invalid')); } const appendLicence = 'powered ' + LabelClaude; return yield* SnsService.snsReply(message.replaceAll("@", ""), `${appendLicence} ${feedTag} `, id2).pipe(Effect.andThen(() => [ { type: "text", text: "posted" } ]), Effect.provide(SnsServiceLive)); }); }; const postSnsWriter = (message) => { return Effect.gen(function* () { if (env.noSnsPost) { const noMes = [ { type: "text", text: env.loggingMode ? "posted to log." : "Posting to SNS is stopped by env settings." } ]; return yield* Effect.succeed(noMes).pipe(Effect.tap(() => env.loggingMode && McpLogService.logTrace(message))); } const img = yield* ImageService.getRecentImageAndClear().pipe(Effect.tap(a => McpLogService.logTrace(`sns image:${a !== undefined}`))); const recentUseLabels = [LabelClaude]; if (useAiImageGen) { recentUseLabels.push(LabelGoogleMap, labelImage(useAiImageGen)); } const appendLicence = 'powered ' + recentUseLabels.join(','); return yield* SnsService.snsPost(message.replaceAll("@", ""), `${appendLicence} ${feedTag} `, img ? { buf: img, mime: "image/png" } : undefined).pipe(Effect.andThen(() => [ { type: "text", text: "posted" } ]), Effect.provide(SnsServiceLive)); }); }; const toolSwitch = (request) => { switch (request.params.name) { case "tips": return tips(); case "get_traveler_info": return getTravelerInfo(); case "set_traveler_info": return setTravelerInfo(String(request.params.arguments?.settings)); case "get_setting": return getSetting(); case "set_avatar_prompt": return setAvatarPrompt(String(request.params.arguments?.prompt)); case "reset_avatar_prompt": return resetAvatarPrompt(); case "get_current_view_info": case "get_traveler_view_info": return getCurrentLocationInfo(request.params.arguments?.includePhoto, request.params.arguments?.includeNearbyFacilities); case "reach_a_percentage_of_destination": return getElapsedView(request.params.arguments?.timeElapsedPercentage); case "get_traveler_location": return getCurrentLocationInfo(false, true); case "set_current_location": case "set_traveler_location": return setCurrentLocation(String(request.params.arguments?.address)); case "get_destination_address": case "get_traveler_destination_address": return getDestinationAddress(); case "set_destination_address": case "set_traveler_destination_address": return setDestinationAddress(String(request.params.arguments?.address)); case "start_journey": case "start_traveler_journey": return startJourney(); case "stop_journey": case "stop_traveler_journey": return stopJourney(); case "call_traveler": return setTravelerExist(true); case "kick_traveler": return setTravelerExist(false); case "get_sns_mentions": return getSnsMentions(); case "read_sns_reader": return readSnsReader(); case "get_sns_feeds": return getSnsFeeds(); case "post_sns_writer": return postSnsWriter(String(request.params.arguments?.message)); case "reply_sns_writer": return replySnsWriter(String(request.params.arguments?.message), String(request.params.arguments?.id)); case "add_like": return addLike(String(request.params.arguments?.id)); default: return Effect.fail(new Error(`Unknown tool:${request.params.name}`)); } }; const run = () => { server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: "file:///role.txt", mimeType: "text/plain", name: "role.txt", description: "The purpose and role of AI" }, { uri: "file:///roleWithSns.txt", mimeType: "text/plain", name: "roleWithSns.txt", description: "The purpose and role of AI with SNS" }, { uri: "file:///carBattle.txt", mimeType: "text/plain", name: "carBattle.txt", description: "Play the fantasy role playing" }, { uri: "file:///credit.txt", mimeType: "text/plain", name: "credit.txt", description: "credit of this component" }, { uri: "file:///setting.txt", mimeType: "text/plain", name: "setting.txt", description: "setting of traveler" } ] }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); return await StoryService.getSettingResource(url.pathname).pipe(Effect.andThen(a => ({ contents: [{ uri: request.params.uri, mimeType: "text/plain", text: a }] })), Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive]), Effect.runPromise).catch(e => { if (e instanceof Error) { throw new Error(e.message); } throw e; }); }); server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: "tips", description: "Inform you of recommended actions for traveler", } ] }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name !== "tips") { throw new Error("Unknown prompt"); } return { messages: [ { role: "user", content: { type: "text", text: "Get tips information." } } ] }; }); server.setRequestHandler(ListToolsRequestSchema, async () => { return await makeToolsDef().pipe(Effect.runPromise); }); server.setRequestHandler(CallToolRequestSchema, async (request) => { return await McpLogService.logTrace(request.params.name).pipe(Effect.andThen(() => { logSync('request.params:', JSON.stringify(request.params)); env.progressToken = request.params._meta?.progressToken; return toolSwitch(request); }), Effect.andThen(a => noImageOut ? a.filter(v => v.type !== 'image') : a), Effect.provide([DbServiceLive, ImageServiceLive]), Effect.andThen(a => ({ content: a })), Effect.catchIf(a => a instanceof AnswerError, e => { return Effect.succeed({ content: [{ type: "text", text: e.message }] }); }), Effect.catchAll(e => { return McpLogService.logError(`catch all:${e.toString()},${JSON.stringify(e)}`).pipe(Effect.as({ isError: true, content: [{ type: "text", text: "Sorry,unknown system error." }] }), Effect.provide(McpLogServiceLive)); }), Effect.runPromise); }); const transport = new StdioServerTransport(); const p = Effect.gen(function* () { yield* Effect.forkDaemon(DbService.initSystemMode()); yield* Effect.tryPromise({ try: () => { return server.connect(transport); }, catch: error => { return new Error(`mcp server error:${error}`); } }); }).pipe(Effect.provide(DbServiceLive)); return Effect.runFork(p); }; return { tips, run, sendLoggingMessage, setPersonMode, getTravelerInfo, setTravelerInfo, getCurrentLocationInfo, setCurrentLocation, getDestinationAddress, setDestinationAddress, startJourney, stopJourney, getSnsFeeds, getSnsMentions, replySnsWriter, toolSwitch, getSetting, makeToolsDef, }; }) }) { } export const McpServiceLive = McpService.Default;