mirror of
https://github.com/kennethnym/aris.git
synced 2026-06-14 11:31:17 +01:00
Compare commits
2 Commits
feat/web-s
...
feat/googl
| Author | SHA1 | Date | |
|---|---|---|---|
|
ac90d46c2a
|
|||
| ef7301ab18 |
@@ -66,11 +66,17 @@ export function SourceConfigPanel({ source, onUpdate }: SourceConfigPanelProps)
|
|||||||
return creds
|
return creds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasUserConfigFields(): boolean {
|
||||||
|
return Object.values(source.fields).some((field) => !isCredentialField(field))
|
||||||
|
}
|
||||||
|
|
||||||
function buildReplaceBody(enabledValue: boolean): Parameters<typeof replaceSource>[1] {
|
function buildReplaceBody(enabledValue: boolean): Parameters<typeof replaceSource>[1] {
|
||||||
const body: Parameters<typeof replaceSource>[1] = { enabled: enabledValue }
|
const body: Parameters<typeof replaceSource>[1] = { enabled: enabledValue }
|
||||||
if (Object.keys(source.fields).length > 0) {
|
|
||||||
|
if (hasUserConfigFields()) {
|
||||||
body.config = getUserConfig()
|
body.config = getUserConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,12 @@ const sourceDefinitions: SourceDefinition[] = [
|
|||||||
description: "Exa web search action. Requires EXA_API_KEY on the backend.",
|
description: "Exa web search action. Requires EXA_API_KEY on the backend.",
|
||||||
fields: {},
|
fields: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "freya.google-maps",
|
||||||
|
name: "Google Maps",
|
||||||
|
description: "Google Maps Grounding Lite MCP tools for places, weather, routes, and Place IDs.",
|
||||||
|
fields: {},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function fetchSources(): Promise<SourceDefinition[]> {
|
export function fetchSources(): Promise<SourceDefinition[]> {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
TrainFront,
|
TrainFront,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
Map as MapIcon,
|
||||||
MapPin,
|
MapPin,
|
||||||
Rss,
|
Rss,
|
||||||
Server,
|
Server,
|
||||||
@@ -49,6 +50,7 @@ const SOURCE_ICONS: Record<string, React.ComponentType<{ className?: string }>>
|
|||||||
"freya.weather": CloudSun,
|
"freya.weather": CloudSun,
|
||||||
"freya.caldav": CalendarDays,
|
"freya.caldav": CalendarDays,
|
||||||
"freya.google-calendar": Calendar,
|
"freya.google-calendar": Calendar,
|
||||||
|
"freya.google-maps": MapIcon,
|
||||||
"freya.tfl": TrainFront,
|
"freya.tfl": TrainFront,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@freya/core": "workspace:*",
|
"@freya/core": "workspace:*",
|
||||||
"@freya/source-caldav": "workspace:*",
|
"@freya/source-caldav": "workspace:*",
|
||||||
"@freya/source-google-calendar": "workspace:*",
|
"@freya/source-google-calendar": "workspace:*",
|
||||||
|
"@freya/source-google-maps": "workspace:*",
|
||||||
"@freya/source-location": "workspace:*",
|
"@freya/source-location": "workspace:*",
|
||||||
"@freya/source-tfl": "workspace:*",
|
"@freya/source-tfl": "workspace:*",
|
||||||
"@freya/source-weatherkit": "workspace:*",
|
"@freya/source-weatherkit": "workspace:*",
|
||||||
|
|||||||
55
apps/freya-backend/src/google-maps/provider.test.ts
Normal file
55
apps/freya-backend/src/google-maps/provider.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { GoogleMapsSourceOptions } from "@freya/source-google-maps"
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { GoogleMapsSourceProvider } from "./provider.ts"
|
||||||
|
|
||||||
|
type McpClient = NonNullable<GoogleMapsSourceOptions["client"]>
|
||||||
|
|
||||||
|
class MockMcpClient implements McpClient {
|
||||||
|
async listTools(): ReturnType<McpClient["listTools"]> {
|
||||||
|
return { tools: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
async readResource(
|
||||||
|
_params: Parameters<McpClient["readResource"]>[0],
|
||||||
|
): ReturnType<McpClient["readResource"]> {
|
||||||
|
throw new Error("unexpected resource read")
|
||||||
|
}
|
||||||
|
|
||||||
|
async callTool(_params: Parameters<McpClient["callTool"]>[0]): ReturnType<McpClient["callTool"]> {
|
||||||
|
return { structuredContent: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GoogleMapsSourceProvider", () => {
|
||||||
|
test("sourceId is freya.google-maps", () => {
|
||||||
|
const provider = new GoogleMapsSourceProvider({ apiKey: "key" })
|
||||||
|
expect(provider.sourceId).toBe("freya.google-maps")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws when service API key is empty", () => {
|
||||||
|
expect(() => new GoogleMapsSourceProvider({ apiKey: "" })).toThrow(
|
||||||
|
"Google Maps MCP API key must be configured",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns source with service API key", async () => {
|
||||||
|
const provider = new GoogleMapsSourceProvider({ apiKey: "key" })
|
||||||
|
|
||||||
|
const source = await provider.feedSourceForUser("user-1", {}, null)
|
||||||
|
|
||||||
|
expect(source.id).toBe("freya.google-maps")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows injected test client with service API key", async () => {
|
||||||
|
const provider = new GoogleMapsSourceProvider({
|
||||||
|
apiKey: "key",
|
||||||
|
client: new MockMcpClient(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const source = await provider.feedSourceForUser("user-1", {}, null)
|
||||||
|
|
||||||
|
expect(source.id).toBe("freya.google-maps")
|
||||||
|
})
|
||||||
|
})
|
||||||
39
apps/freya-backend/src/google-maps/provider.ts
Normal file
39
apps/freya-backend/src/google-maps/provider.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { GoogleMapsSource, type GoogleMapsSourceOptions } from "@freya/source-google-maps"
|
||||||
|
|
||||||
|
import type { FeedSourceProvider } from "../session/feed-source-provider.ts"
|
||||||
|
|
||||||
|
export interface GoogleMapsSourceProviderOptions {
|
||||||
|
readonly apiKey: string
|
||||||
|
readonly client?: GoogleMapsSourceOptions["client"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GoogleMapsSourceProvider implements FeedSourceProvider {
|
||||||
|
readonly sourceId = "freya.google-maps"
|
||||||
|
|
||||||
|
private readonly apiKey: string
|
||||||
|
private readonly client: GoogleMapsSourceProviderOptions["client"]
|
||||||
|
|
||||||
|
constructor(options: GoogleMapsSourceProviderOptions) {
|
||||||
|
if (!nonEmptyString(options.apiKey)) {
|
||||||
|
throw new Error("Google Maps MCP API key must be configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiKey = options.apiKey
|
||||||
|
this.client = options.client
|
||||||
|
}
|
||||||
|
|
||||||
|
async feedSourceForUser(
|
||||||
|
_userId: string,
|
||||||
|
_config: unknown,
|
||||||
|
_credentials: unknown,
|
||||||
|
): Promise<GoogleMapsSource> {
|
||||||
|
return new GoogleMapsSource({
|
||||||
|
apiKey: this.apiKey,
|
||||||
|
client: this.client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonEmptyString(value: string): boolean {
|
||||||
|
return typeof value === "string" && value.trim().length > 0
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { createDatabase } from "./db/index.ts"
|
|||||||
import { registerFeedHttpHandlers } from "./engine/http.ts"
|
import { registerFeedHttpHandlers } from "./engine/http.ts"
|
||||||
import { createFeedEnhancer } from "./enhancement/enhance-feed.ts"
|
import { createFeedEnhancer } from "./enhancement/enhance-feed.ts"
|
||||||
import { createLlmClient } from "./enhancement/llm-client.ts"
|
import { createLlmClient } from "./enhancement/llm-client.ts"
|
||||||
|
import { GoogleMapsSourceProvider } from "./google-maps/provider.ts"
|
||||||
import { CredentialEncryptor } from "./lib/crypto.ts"
|
import { CredentialEncryptor } from "./lib/crypto.ts"
|
||||||
import { registerLocationHttpHandlers } from "./location/http.ts"
|
import { registerLocationHttpHandlers } from "./location/http.ts"
|
||||||
import { LocationSourceProvider } from "./location/provider.ts"
|
import { LocationSourceProvider } from "./location/provider.ts"
|
||||||
@@ -47,6 +48,11 @@ function main() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY ?? process.env.GOOGLE_MAPS_MCP_API_KEY
|
||||||
|
if (!googleMapsApiKey) {
|
||||||
|
throw new Error("GOOGLE_MAPS_API_KEY or GOOGLE_MAPS_MCP_API_KEY must be set")
|
||||||
|
}
|
||||||
|
|
||||||
const sessionManager = new UserSessionManager({
|
const sessionManager = new UserSessionManager({
|
||||||
db,
|
db,
|
||||||
providers: [
|
providers: [
|
||||||
@@ -62,6 +68,9 @@ function main() {
|
|||||||
}),
|
}),
|
||||||
new TflSourceProvider({ apiKey: process.env.TFL_API_KEY! }),
|
new TflSourceProvider({ apiKey: process.env.TFL_API_KEY! }),
|
||||||
new WebSearchSourceProvider({ apiKey: process.env.EXA_API_KEY }),
|
new WebSearchSourceProvider({ apiKey: process.env.EXA_API_KEY }),
|
||||||
|
new GoogleMapsSourceProvider({
|
||||||
|
apiKey: googleMapsApiKey,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
feedEnhancer,
|
feedEnhancer,
|
||||||
credentialEncryptor,
|
credentialEncryptor,
|
||||||
|
|||||||
11
bun.lock
11
bun.lock
@@ -53,6 +53,7 @@
|
|||||||
"@freya/core": "workspace:*",
|
"@freya/core": "workspace:*",
|
||||||
"@freya/source-caldav": "workspace:*",
|
"@freya/source-caldav": "workspace:*",
|
||||||
"@freya/source-google-calendar": "workspace:*",
|
"@freya/source-google-calendar": "workspace:*",
|
||||||
|
"@freya/source-google-maps": "workspace:*",
|
||||||
"@freya/source-location": "workspace:*",
|
"@freya/source-location": "workspace:*",
|
||||||
"@freya/source-tfl": "workspace:*",
|
"@freya/source-tfl": "workspace:*",
|
||||||
"@freya/source-weatherkit": "workspace:*",
|
"@freya/source-weatherkit": "workspace:*",
|
||||||
@@ -193,6 +194,14 @@
|
|||||||
"arktype": "^2.1.0",
|
"arktype": "^2.1.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/freya-source-google-maps": {
|
||||||
|
"name": "@freya/source-google-maps",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@freya/source-mcp": "workspace:*",
|
||||||
|
"arktype": "^2.1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/freya-source-location": {
|
"packages/freya-source-location": {
|
||||||
"name": "@freya/source-location",
|
"name": "@freya/source-location",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -672,6 +681,8 @@
|
|||||||
|
|
||||||
"@freya/source-google-calendar": ["@freya/source-google-calendar@workspace:packages/freya-source-google-calendar"],
|
"@freya/source-google-calendar": ["@freya/source-google-calendar@workspace:packages/freya-source-google-calendar"],
|
||||||
|
|
||||||
|
"@freya/source-google-maps": ["@freya/source-google-maps@workspace:packages/freya-source-google-maps"],
|
||||||
|
|
||||||
"@freya/source-location": ["@freya/source-location@workspace:packages/freya-source-location"],
|
"@freya/source-location": ["@freya/source-location@workspace:packages/freya-source-location"],
|
||||||
|
|
||||||
"@freya/source-tfl": ["@freya/source-tfl@workspace:packages/freya-source-tfl"],
|
"@freya/source-tfl": ["@freya/source-tfl@workspace:packages/freya-source-tfl"],
|
||||||
|
|||||||
14
packages/freya-source-google-maps/package.json
Normal file
14
packages/freya-source-google-maps/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@freya/source-google-maps",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@freya/source-mcp": "workspace:*",
|
||||||
|
"arktype": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
135
packages/freya-source-google-maps/src/google-maps-source.test.ts
Normal file
135
packages/freya-source-google-maps/src/google-maps-source.test.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import type {
|
||||||
|
McpCallToolParams,
|
||||||
|
McpCallToolResult,
|
||||||
|
McpClient,
|
||||||
|
McpListToolsResult,
|
||||||
|
McpReadResourceParams,
|
||||||
|
McpReadResourceResult,
|
||||||
|
} from "@freya/source-mcp"
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { GoogleMapsAction, GoogleMapsSource, GoogleMapsSourceId, GoogleMapsTool } from "./index"
|
||||||
|
|
||||||
|
class MockMcpClient implements McpClient {
|
||||||
|
readonly calls: McpCallToolParams[] = []
|
||||||
|
|
||||||
|
async listTools(): Promise<McpListToolsResult> {
|
||||||
|
return {
|
||||||
|
tools: Object.values(GoogleMapsTool).map((name) => ({
|
||||||
|
name,
|
||||||
|
description: `${name} description`,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readResource(_params: McpReadResourceParams): Promise<McpReadResourceResult> {
|
||||||
|
throw new Error("unexpected resource read")
|
||||||
|
}
|
||||||
|
|
||||||
|
async callTool(params: McpCallToolParams): Promise<McpCallToolResult> {
|
||||||
|
this.calls.push(params)
|
||||||
|
return {
|
||||||
|
structuredContent: {
|
||||||
|
tool: params.name,
|
||||||
|
arguments: params.arguments ?? {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GoogleMapsSource", () => {
|
||||||
|
test("uses the Google Maps source id", () => {
|
||||||
|
const source = new GoogleMapsSource({ client: new MockMcpClient() })
|
||||||
|
expect(source.id).toBe(GoogleMapsSourceId)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("exposes documented Google Maps MCP tools as actions", async () => {
|
||||||
|
const source = new GoogleMapsSource({ client: new MockMcpClient() })
|
||||||
|
|
||||||
|
const actions = await source.listActions()
|
||||||
|
|
||||||
|
expect(Object.keys(actions).sort()).toEqual(Object.values(GoogleMapsAction).sort())
|
||||||
|
expect(actions[GoogleMapsAction.SearchPlaces]!.id).toBe(GoogleMapsAction.SearchPlaces)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("maps action execution to the underlying MCP tool", async () => {
|
||||||
|
const client = new MockMcpClient()
|
||||||
|
const source = new GoogleMapsSource({ client })
|
||||||
|
|
||||||
|
const result = await source.executeAction(GoogleMapsAction.SearchPlaces, {
|
||||||
|
textQuery: "coffee shops near Golden Gate Park",
|
||||||
|
regionCode: "US",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(client.calls).toEqual([
|
||||||
|
{
|
||||||
|
name: GoogleMapsTool.SearchPlaces,
|
||||||
|
arguments: {
|
||||||
|
textQuery: "coffee shops near Golden Gate Park",
|
||||||
|
regionCode: "US",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(result).toEqual({
|
||||||
|
tool: GoogleMapsTool.SearchPlaces,
|
||||||
|
arguments: {
|
||||||
|
textQuery: "coffee shops near Golden Gate Park",
|
||||||
|
regionCode: "US",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("validates action input before calling the MCP tool", async () => {
|
||||||
|
const client = new MockMcpClient()
|
||||||
|
const source = new GoogleMapsSource({ client })
|
||||||
|
|
||||||
|
await expectRejectsWithMessage(
|
||||||
|
source.executeAction(GoogleMapsAction.SearchPlaces, {}),
|
||||||
|
"textQuery must be a string",
|
||||||
|
)
|
||||||
|
expect(client.calls).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("validates resolve names query objects", async () => {
|
||||||
|
const client = new MockMcpClient()
|
||||||
|
const source = new GoogleMapsSource({ client })
|
||||||
|
|
||||||
|
await expectRejectsWithMessage(
|
||||||
|
source.executeAction(GoogleMapsAction.ResolveNames, { queries: [{}] }),
|
||||||
|
"queries[0].text must be a string",
|
||||||
|
)
|
||||||
|
expect(client.calls).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not produce feed items or context by default", async () => {
|
||||||
|
const source = new GoogleMapsSource({ client: new MockMcpClient() })
|
||||||
|
|
||||||
|
const contextEntries = await source.fetchContext(undefined as never)
|
||||||
|
const items = await source.fetchItems(undefined as never)
|
||||||
|
|
||||||
|
expect(contextEntries).toBeNull()
|
||||||
|
expect(items).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function expectRejectsWithMessage(
|
||||||
|
promise: Promise<unknown>,
|
||||||
|
expectedMessage: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
} catch (err) {
|
||||||
|
expect(errorMessage(err)).toContain(expectedMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Expected promise to reject with "${expectedMessage}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return err.message
|
||||||
|
}
|
||||||
|
return String(err)
|
||||||
|
}
|
||||||
151
packages/freya-source-google-maps/src/google-maps-source.ts
Normal file
151
packages/freya-source-google-maps/src/google-maps-source.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
McpSource,
|
||||||
|
type McpActionMapping,
|
||||||
|
type McpClient,
|
||||||
|
type McpHttpHeaders,
|
||||||
|
type McpSourceOptions,
|
||||||
|
} from "@freya/source-mcp"
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComputeRoutesInput,
|
||||||
|
LookupWeatherInput,
|
||||||
|
ResolveMapsUrlsInput,
|
||||||
|
ResolveNamesInput,
|
||||||
|
SearchPlacesInput,
|
||||||
|
} from "./schemas"
|
||||||
|
|
||||||
|
export type GoogleMapsApiKey = string | (() => Promise<string>)
|
||||||
|
|
||||||
|
export interface GoogleMapsSourceOptions {
|
||||||
|
readonly endpoint?: string | URL
|
||||||
|
readonly apiKey?: GoogleMapsApiKey
|
||||||
|
readonly timeoutMs?: number
|
||||||
|
readonly headers?: McpHttpHeaders | (() => Promise<McpHttpHeaders>)
|
||||||
|
readonly requestInit?: RequestInit
|
||||||
|
readonly transportOptions?: McpSourceOptions["transportOptions"]
|
||||||
|
readonly client?: McpClient
|
||||||
|
readonly clientFactory?: McpSourceOptions["clientFactory"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GoogleMapsSourceId = "freya.google-maps"
|
||||||
|
|
||||||
|
export const GoogleMapsMcpEndpoint = "https://mapstools.googleapis.com/mcp"
|
||||||
|
|
||||||
|
export const GoogleMapsAction = {
|
||||||
|
SearchPlaces: "search-places",
|
||||||
|
LookupWeather: "lookup-weather",
|
||||||
|
ComputeRoutes: "compute-routes",
|
||||||
|
ResolveNames: "resolve-names",
|
||||||
|
ResolveMapsUrls: "resolve-maps-urls",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type GoogleMapsAction = (typeof GoogleMapsAction)[keyof typeof GoogleMapsAction]
|
||||||
|
|
||||||
|
export const GoogleMapsTool = {
|
||||||
|
SearchPlaces: "search_places",
|
||||||
|
LookupWeather: "lookup_weather",
|
||||||
|
ComputeRoutes: "compute_routes",
|
||||||
|
ResolveNames: "resolve_names",
|
||||||
|
ResolveMapsUrls: "resolve_maps_urls",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type GoogleMapsTool = (typeof GoogleMapsTool)[keyof typeof GoogleMapsTool]
|
||||||
|
|
||||||
|
const GoogleMapsActions = {
|
||||||
|
[GoogleMapsAction.SearchPlaces]: {
|
||||||
|
tool: GoogleMapsTool.SearchPlaces,
|
||||||
|
description:
|
||||||
|
"Find places, businesses, addresses, locations, and points of interest with Google Maps.",
|
||||||
|
input: SearchPlacesInput,
|
||||||
|
},
|
||||||
|
[GoogleMapsAction.LookupWeather]: {
|
||||||
|
tool: GoogleMapsTool.LookupWeather,
|
||||||
|
description: "Retrieve current conditions and weather forecasts through Google Maps.",
|
||||||
|
input: LookupWeatherInput,
|
||||||
|
},
|
||||||
|
[GoogleMapsAction.ComputeRoutes]: {
|
||||||
|
tool: GoogleMapsTool.ComputeRoutes,
|
||||||
|
description: "Compute a Google Maps route between an origin and destination.",
|
||||||
|
input: ComputeRoutesInput,
|
||||||
|
},
|
||||||
|
[GoogleMapsAction.ResolveNames]: {
|
||||||
|
tool: GoogleMapsTool.ResolveNames,
|
||||||
|
description: "Resolve specific place names or addresses into Google Maps Place IDs.",
|
||||||
|
input: ResolveNamesInput,
|
||||||
|
},
|
||||||
|
[GoogleMapsAction.ResolveMapsUrls]: {
|
||||||
|
tool: GoogleMapsTool.ResolveMapsUrls,
|
||||||
|
description: "Resolve Google Maps URLs into canonical Google Maps Place IDs.",
|
||||||
|
input: ResolveMapsUrlsInput,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<GoogleMapsAction, McpActionMapping>
|
||||||
|
|
||||||
|
export class GoogleMapsSource extends McpSource {
|
||||||
|
constructor(options: GoogleMapsSourceOptions = {}) {
|
||||||
|
super({
|
||||||
|
id: GoogleMapsSourceId,
|
||||||
|
url: options.endpoint ?? GoogleMapsMcpEndpoint,
|
||||||
|
clientName: "freya-source-google-maps",
|
||||||
|
clientVersion: "0.0.0",
|
||||||
|
timeoutMs: options.timeoutMs,
|
||||||
|
headers: createGoogleMapsHeaders({
|
||||||
|
headers: options.headers,
|
||||||
|
apiKey: options.apiKey,
|
||||||
|
}),
|
||||||
|
requestInit: options.requestInit,
|
||||||
|
transportOptions: options.transportOptions,
|
||||||
|
client: options.client,
|
||||||
|
clientFactory: options.clientFactory,
|
||||||
|
actions: GoogleMapsActions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoogleMapsHeaderOptions {
|
||||||
|
readonly headers: McpHttpHeaders | (() => Promise<McpHttpHeaders>) | undefined
|
||||||
|
readonly apiKey: GoogleMapsApiKey | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGoogleMapsHeaders({
|
||||||
|
headers,
|
||||||
|
apiKey,
|
||||||
|
}: GoogleMapsHeaderOptions): McpHttpHeaders | (() => Promise<McpHttpHeaders>) | undefined {
|
||||||
|
if (!apiKey) {
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
const merged = new Headers()
|
||||||
|
const resolvedHeaders = typeof headers === "function" ? await headers() : headers
|
||||||
|
if (resolvedHeaders) {
|
||||||
|
applyHeaders(merged, resolvedHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
const resolvedApiKey = typeof apiKey === "function" ? await apiKey() : apiKey
|
||||||
|
merged.set("x-goog-api-key", resolvedApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHeaders(target: Headers, headers: McpHttpHeaders): void {
|
||||||
|
if (headers instanceof Headers) {
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
target.set(key, value)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(headers)) {
|
||||||
|
for (const [key, value] of headers) {
|
||||||
|
target.set(key, value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
target.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/freya-source-google-maps/src/index.ts
Normal file
17
packages/freya-source-google-maps/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export {
|
||||||
|
GoogleMapsAction,
|
||||||
|
GoogleMapsMcpEndpoint,
|
||||||
|
GoogleMapsSource,
|
||||||
|
GoogleMapsSourceId,
|
||||||
|
GoogleMapsTool,
|
||||||
|
type GoogleMapsApiKey,
|
||||||
|
type GoogleMapsSourceOptions,
|
||||||
|
} from "./google-maps-source"
|
||||||
|
|
||||||
|
export {
|
||||||
|
ComputeRoutesInput,
|
||||||
|
LookupWeatherInput,
|
||||||
|
ResolveMapsUrlsInput,
|
||||||
|
ResolveNamesInput,
|
||||||
|
SearchPlacesInput,
|
||||||
|
} from "./schemas"
|
||||||
41
packages/freya-source-google-maps/src/schemas.ts
Normal file
41
packages/freya-source-google-maps/src/schemas.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { type } from "arktype"
|
||||||
|
|
||||||
|
const ResolveNameQuery = type({
|
||||||
|
"+": "reject",
|
||||||
|
text: "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SearchPlacesInput = type({
|
||||||
|
"+": "reject",
|
||||||
|
textQuery: "string",
|
||||||
|
"locationBias?": "unknown",
|
||||||
|
"languageCode?": "string",
|
||||||
|
"regionCode?": "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LookupWeatherInput = type({
|
||||||
|
"+": "reject",
|
||||||
|
location: "unknown",
|
||||||
|
"date?": "unknown",
|
||||||
|
"hour?": "number",
|
||||||
|
"unitsSystem?": "'UNITS_SYSTEM_UNSPECIFIED' | 'METRIC' | 'IMPERIAL'",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ComputeRoutesInput = type({
|
||||||
|
"+": "reject",
|
||||||
|
origin: "unknown",
|
||||||
|
destination: "unknown",
|
||||||
|
"travelMode?": "'ROUTE_TRAVEL_MODE_UNSPECIFIED' | 'DRIVE' | 'WALK'",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ResolveNamesInput = type({
|
||||||
|
"+": "reject",
|
||||||
|
queries: ResolveNameQuery.array(),
|
||||||
|
"locationBias?": "unknown",
|
||||||
|
"regionCode?": "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ResolveMapsUrlsInput = type({
|
||||||
|
"+": "reject",
|
||||||
|
urls: "string[]",
|
||||||
|
})
|
||||||
4
packages/freya-source-google-maps/tsconfig.json
Normal file
4
packages/freya-source-google-maps/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user