11import z from "zod"
2+ import { Effect } from "effect"
3+ import { HttpClient } from "effect/unstable/http"
24import { Tool } from "./tool"
5+ import * as McpExa from "./mcp-exa"
36import DESCRIPTION from "./websearch.txt"
4- import { abortAfterAny } from "../util/abort"
5-
6- const API_CONFIG = {
7- BASE_URL : "https://mcp.exa.ai" ,
8- ENDPOINTS : {
9- SEARCH : "/mcp" ,
10- } ,
11- DEFAULT_NUM_RESULTS : 8 ,
12- } as const
137
148const Parameters = z . object ( {
159 query : z . string ( ) . describe ( "Websearch query" ) ,
@@ -30,121 +24,53 @@ const Parameters = z.object({
3024 . describe ( "Maximum characters for context string optimized for LLMs (default: 10000)" ) ,
3125} )
3226
33- interface McpSearchRequest {
34- jsonrpc : string
35- id : number
36- method : string
37- params : {
38- name : string
39- arguments : {
40- query : string
41- numResults ?: number
42- livecrawl ?: "fallback" | "preferred"
43- type ?: "auto" | "fast" | "deep"
44- contextMaxCharacters ?: number
45- }
46- }
47- }
48-
49- interface McpSearchResponse {
50- jsonrpc : string
51- result : {
52- content : Array < {
53- type : string
54- text : string
55- } >
56- }
57- }
58-
59- export const WebSearchTool = Tool . define ( "websearch" , async ( ) => {
60- return {
61- get description ( ) {
62- return DESCRIPTION . replace ( "{{year}}" , new Date ( ) . getFullYear ( ) . toString ( ) )
63- } ,
64- parameters : Parameters ,
65- async execute ( params , ctx ) {
66- await ctx . ask ( {
67- permission : "websearch" ,
68- patterns : [ params . query ] ,
69- always : [ "*" ] ,
70- metadata : {
71- query : params . query ,
72- numResults : params . numResults ,
73- livecrawl : params . livecrawl ,
74- type : params . type ,
75- contextMaxCharacters : params . contextMaxCharacters ,
76- } ,
77- } )
78-
79- const searchRequest : McpSearchRequest = {
80- jsonrpc : "2.0" ,
81- id : 1 ,
82- method : "tools/call" ,
83- params : {
84- name : "web_search_exa" ,
85- arguments : {
86- query : params . query ,
87- type : params . type || "auto" ,
88- numResults : params . numResults || API_CONFIG . DEFAULT_NUM_RESULTS ,
89- livecrawl : params . livecrawl || "fallback" ,
90- contextMaxCharacters : params . contextMaxCharacters ,
91- } ,
92- } ,
93- }
94-
95- const { signal, clearTimeout } = abortAfterAny ( 25000 , ctx . abort )
96-
97- try {
98- const headers : Record < string , string > = {
99- accept : "application/json, text/event-stream" ,
100- "content-type" : "application/json" ,
101- }
102-
103- const response = await fetch ( `${ API_CONFIG . BASE_URL } ${ API_CONFIG . ENDPOINTS . SEARCH } ` , {
104- method : "POST" ,
105- headers,
106- body : JSON . stringify ( searchRequest ) ,
107- signal,
108- } )
109-
110- clearTimeout ( )
111-
112- if ( ! response . ok ) {
113- const errorText = await response . text ( )
114- throw new Error ( `Search error (${ response . status } ): ${ errorText } ` )
115- }
116-
117- const responseText = await response . text ( )
118-
119- // Parse SSE response
120- const lines = responseText . split ( "\n" )
121- for ( const line of lines ) {
122- if ( line . startsWith ( "data: " ) ) {
123- const data : McpSearchResponse = JSON . parse ( line . substring ( 6 ) )
124- if ( data . result && data . result . content && data . result . content . length > 0 ) {
125- return {
126- output : data . result . content [ 0 ] . text ,
127- title : `Web search: ${ params . query } ` ,
128- metadata : { } ,
129- }
130- }
27+ export const WebSearchTool = Tool . defineEffect (
28+ "websearch" ,
29+ Effect . gen ( function * ( ) {
30+ const http = yield * HttpClient . HttpClient
31+
32+ return {
33+ get description ( ) {
34+ return DESCRIPTION . replace ( "{{year}}" , new Date ( ) . getFullYear ( ) . toString ( ) )
35+ } ,
36+ parameters : Parameters ,
37+ execute : ( params : z . infer < typeof Parameters > , ctx : Tool . Context ) =>
38+ Effect . gen ( function * ( ) {
39+ yield * Effect . promise ( ( ) =>
40+ ctx . ask ( {
41+ permission : "websearch" ,
42+ patterns : [ params . query ] ,
43+ always : [ "*" ] ,
44+ metadata : {
45+ query : params . query ,
46+ numResults : params . numResults ,
47+ livecrawl : params . livecrawl ,
48+ type : params . type ,
49+ contextMaxCharacters : params . contextMaxCharacters ,
50+ } ,
51+ } ) ,
52+ )
53+
54+ const result = yield * McpExa . call (
55+ http ,
56+ "web_search_exa" ,
57+ McpExa . SearchArgs ,
58+ {
59+ query : params . query ,
60+ type : params . type || "auto" ,
61+ numResults : params . numResults || 8 ,
62+ livecrawl : params . livecrawl || "fallback" ,
63+ contextMaxCharacters : params . contextMaxCharacters ,
64+ } ,
65+ "25 seconds" ,
66+ )
67+
68+ return {
69+ output : result ?? "No search results found. Please try a different query." ,
70+ title : `Web search: ${ params . query } ` ,
71+ metadata : { } ,
13172 }
132- }
133-
134- return {
135- output : "No search results found. Please try a different query." ,
136- title : `Web search: ${ params . query } ` ,
137- metadata : { } ,
138- }
139- } catch ( error ) {
140- clearTimeout ( )
141-
142- if ( error instanceof Error && error . name === "AbortError" ) {
143- throw new Error ( "Search request timed out" )
144- }
145-
146- throw error
147- }
148- } ,
149- }
150- } )
73+ } ) . pipe ( Effect . runPromise ) ,
74+ }
75+ } ) ,
76+ )
0 commit comments