Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion squirreling-gis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
"typecheck": "tsc"
},
"dependencies": {
"@mcp-b/global": "^2.2.0",
"@mcp-b/webmcp-types": "^2.2.0",
"hyparquet": "1.25.6",
"hyparquet-compressors": "1.1.1",
"hyperparam": "0.4.9",
"leaflet": "1.9.4",
"react": "19.2.5",
"react-dom": "19.2.5",
"squirreling": "0.12.1"
"squirreling": "0.12.1",
"@mcp-b/react-webmcp": "^2.2.0"
},
"devDependencies": {
"@types/leaflet": "1.9.21",
Expand Down
4 changes: 2 additions & 2 deletions squirreling-gis/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function App(): ReactNode {
const url = params.get('key') ?? undefined

const [error, setError] = useState<Error>()
const [pageProps, setPageProps] = useState<PageProps>()
const [pageProps, setPageProps] = useState<Omit<PageProps, 'loadUrl'>>()

const setUnknownError = useCallback((e: unknown) => {
setError(e === undefined || e instanceof Error ? e : new Error('Unknown error' + JSON.stringify(e)))
Expand Down Expand Up @@ -53,7 +53,7 @@ export default function App(): ReactNode {
onError={(e) => { setError(e) }}
onFileDrop={onFileDrop}
onUrlDrop={onUrlDrop}>
{pageProps ? <Page {...pageProps} /> : <Welcome setUrl={onUrlDrop} />}
{pageProps ? <Page {...pageProps} loadUrl={onUrlDrop} /> : <Welcome setUrl={onUrlDrop} />}
</Dropzone>
</Layout>
}
62 changes: 60 additions & 2 deletions squirreling-gis/src/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FileMetaData, cachedAsyncBuffer, parquetMetadataAsync } from 'hyparquet'
import type { Geometry } from 'hyparquet/src/types.js'
import { AsyncBufferFrom, asyncBufferFrom } from 'hyperparam'
import { AsyncBufferFrom, asyncBufferFrom, Json } from 'hyperparam';
import { compressors } from 'hyparquet-compressors'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { AsyncDataSource, AsyncRow, cachedDataSource, executePlan, parseSql, planSql } from 'squirreling'
Expand All @@ -9,6 +9,8 @@ import { countingBuffer } from './countingBuffer.js'
import { HighlightedTextArea } from './HighlightedTextArea.js'
import { highlightSql } from './sqlHighlight.js'
import LeafletMap, { MapFeature } from './LeafletMap.js'
import { useWebMCP } from '@mcp-b/react-webmcp'
import type { JsonSchemaForInference } from '@mcp-b/webmcp-types'

const exampleQueries = [
{
Expand Down Expand Up @@ -41,13 +43,14 @@ export interface PageProps {
from: AsyncBufferFrom
byteLength?: number
setError: (e: unknown) => void
loadUrl: (url: string) => void
}

/**
* Squirreling GIS demo page.
* Enter SQL queries to filter geospatial data and view results on a map.
*/
export default function Page({ metadata, name, from, byteLength, setError }: PageProps): ReactNode {
export default function Page({ metadata, name, from, byteLength, setError, loadUrl }: PageProps): ReactNode {
const [query, setQuery] = useState<string>(exampleQueries[0].query)
const [features, setFeatures] = useState<MapFeature[]>([])
const [featureCount, setFeatureCount] = useState(0)
Expand All @@ -68,6 +71,61 @@ export default function Page({ metadata, name, from, byteLength, setError }: Pag
setSqlError(undefined)
}, [setError])

useWebMCP({
name: 'run_sql',
description: 'Run a SQL query against the loaded GeoParquet file. Updates the map with results. Table is named "table". Supports ST_WITHIN, ST_MAKEENVELOPE, ST_GEOMFROMTEXT. Always use LIMIT. Example: SELECT * FROM table WHERE ST_WITHIN(geometry, ST_MAKEENVELOPE(-122.5, 37.7, -122.3, 37.8)) LIMIT 1000',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'SQL query to execute' },
},
required: ['query'],
} as const satisfies JsonSchemaForInference,
handler: async ({ query: sql }) => {
if (!table) throw new Error('No parquet file loaded yet')
handleQueryChange(sql)
return { status: 'query_submitted', query: sql }
},
}, [table, handleQueryChange])

useWebMCP({
name: 'get_map_state',
description: 'Get the current map state: feature count, active query, columns, and sample features.',
inputSchema: {
type: 'object',
properties: {},
} as const satisfies JsonSchemaForInference,
annotations: { readOnlyHint: true },
handler: async () => {
const columns = features.length > 0 ? Object.keys(features[0].properties) : []
return {
featureCount: features.length,
currentQuery: query,
columns,
sampleFeatures: features.slice(0, 5).map(f => ({
geometryType: f.geometry.type,
properties: f.properties,
})),
}
},
}, [features, query])

useWebMCP({
name: 'load_parquet_url',
description: 'Load a different GeoParquet file from a URL (must support HTTP Range requests).',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to a parquet file' },
},
required: ['url'],
} as const satisfies JsonSchemaForInference,
handler: async ({ url }) => {
loadUrl(url)
return { status: 'loading', url }
},
}, [loadUrl])

// Execute query and collect features
useEffect(() => {
if (query.length <= 2 || !table) return
Expand Down
6 changes: 2 additions & 4 deletions squirreling-gis/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import '@mcp-b/global'
import 'leaflet/dist/leaflet.css'
import 'hyperparam/global.css'
import 'hyperparam/hyperparam.css'
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.js'
import './index.css'

const app = document.getElementById('app')
if (!app) throw new Error('missing app element')

ReactDOM.createRoot(app).render(<StrictMode>
<App />
</StrictMode>)
ReactDOM.createRoot(app).render(<App />)
Loading