@@ -32,6 +32,7 @@ import {
3232import { fileExistsAtPath } from "../../utils/fs"
3333import { arePathsEqual } from "../../utils/path"
3434import { injectVariables } from "../../utils/config"
35+ import { SchemaPinService } from "../schemapin"
3536
3637export type McpConnection = {
3738 server : McpServer
@@ -133,15 +134,49 @@ export class McpHub {
133134 isConnecting : boolean = false
134135 private refCount : number = 0 // Reference counter for active clients
135136 private configChangeDebounceTimers : Map < string , NodeJS . Timeout > = new Map ( )
137+ private schemaPinService ?: SchemaPinService
136138
137139 constructor ( provider : ClineProvider ) {
138140 this . providerRef = new WeakRef ( provider )
141+ this . initializeSchemaPinService ( ) . catch ( console . error )
139142 this . watchMcpSettingsFile ( )
140143 this . watchProjectMcpFile ( ) . catch ( console . error )
141144 this . setupWorkspaceFoldersWatcher ( )
142145 this . initializeGlobalMcpServers ( )
143146 this . initializeProjectMcpServers ( )
144147 }
148+
149+ /**
150+ * Initialize SchemaPin service for schema verification
151+ */
152+ private async initializeSchemaPinService ( ) : Promise < void > {
153+ try {
154+ const provider = this . providerRef . deref ( )
155+ if ( ! provider ) {
156+ console . warn ( "Provider not available for SchemaPin initialization" )
157+ return
158+ }
159+
160+ // Get SchemaPin configuration from VSCode settings
161+ const config = vscode . workspace . getConfiguration ( "roo-cline.schemapin" )
162+ const schemaPinConfig = {
163+ enabled : config . get < boolean > ( "enabled" , true ) ,
164+ autoPin : config . get < boolean > ( "autoPin" , false ) ,
165+ timeout : config . get < number > ( "verificationTimeout" , 5000 ) ,
166+ verifyOnToolCall : ! config . get < boolean > ( "strictMode" , false ) , // In strict mode, we verify on tool call
167+ }
168+
169+ if ( schemaPinConfig . enabled ) {
170+ this . schemaPinService = new SchemaPinService ( provider . context , schemaPinConfig )
171+ await this . schemaPinService . initialize ( )
172+ console . log ( "SchemaPin service initialized successfully" )
173+ }
174+ } catch ( error ) {
175+ console . error ( "Failed to initialize SchemaPin service:" , error )
176+ // Don't throw - SchemaPin is optional
177+ }
178+ }
179+
145180 /**
146181 * Registers a client (e.g., ClineProvider) using this hub.
147182 * Increments the reference count.
@@ -1457,6 +1492,17 @@ export class McpHub {
14571492 throw new Error ( `Server "${ serverName } " is disabled and cannot be used` )
14581493 }
14591494
1495+ // SchemaPin verification for tool calls
1496+ if ( this . schemaPinService && this . schemaPinService . isEnabled ( ) ) {
1497+ try {
1498+ await this . verifyToolSchema ( serverName , toolName , source )
1499+ } catch ( error ) {
1500+ console . warn ( `SchemaPin verification failed for ${ serverName } /${ toolName } :` , error )
1501+ // In non-strict mode, we continue with the tool call
1502+ // In strict mode, this would throw an error
1503+ }
1504+ }
1505+
14601506 let timeout : number
14611507 try {
14621508 const parsedConfig = ServerConfigSchema . parse ( JSON . parse ( connection . server . config ) )
@@ -1563,6 +1609,124 @@ export class McpHub {
15631609 }
15641610 }
15651611
1612+ /**
1613+ * Verify tool schema using SchemaPin
1614+ */
1615+ private async verifyToolSchema ( serverName : string , toolName : string , source ?: "global" | "project" ) : Promise < void > {
1616+ if ( ! this . schemaPinService ) {
1617+ return
1618+ }
1619+
1620+ const connection = this . findConnection ( serverName , source )
1621+ if ( ! connection ) {
1622+ return
1623+ }
1624+
1625+ // Find the tool in the server's tools list
1626+ const tool = connection . server . tools ?. find ( ( t ) => t . name === toolName )
1627+ if ( ! tool || ! tool . inputSchema ) {
1628+ return
1629+ }
1630+
1631+ // Check for signed schema file
1632+ const signedSchemaPath = await this . findSignedSchemaFile ( serverName , toolName , source )
1633+ if ( ! signedSchemaPath ) {
1634+ // No signed schema found - this is okay in non-strict mode
1635+ return
1636+ }
1637+
1638+ try {
1639+ const signedSchemaContent = await fs . readFile ( signedSchemaPath , "utf-8" )
1640+ const signedSchema = JSON . parse ( signedSchemaContent )
1641+
1642+ if ( ! signedSchema . signature ) {
1643+ console . warn ( `No signature found in signed schema file for ${ serverName } /${ toolName } ` )
1644+ return
1645+ }
1646+
1647+ // Perform SchemaPin verification
1648+ const verificationResult = await this . schemaPinService . verifyMcpTool ( {
1649+ serverName,
1650+ toolName,
1651+ schema : tool . inputSchema as Record < string , unknown > ,
1652+ signature : signedSchema . signature ,
1653+ domain : this . extractDomainFromServerName ( serverName ) ,
1654+ } )
1655+
1656+ if ( ! verificationResult . valid ) {
1657+ throw new Error ( `Schema verification failed: ${ verificationResult . error } ` )
1658+ }
1659+
1660+ console . log ( `SchemaPin verification successful for ${ serverName } /${ toolName } ` )
1661+ } catch ( error ) {
1662+ console . error ( `SchemaPin verification error for ${ serverName } /${ toolName } :` , error )
1663+ throw error
1664+ }
1665+ }
1666+
1667+ /**
1668+ * Find signed schema file for a tool
1669+ */
1670+ private async findSignedSchemaFile (
1671+ serverName : string ,
1672+ toolName : string ,
1673+ source ?: "global" | "project" ,
1674+ ) : Promise < string | null > {
1675+ // Look for .signed.schema.json files in common locations
1676+ const possiblePaths = [
1677+ // Next to the server executable
1678+ `${ serverName } /${ toolName } .signed.schema.json` ,
1679+ `${ serverName } .${ toolName } .signed.schema.json` ,
1680+ // In a schemas directory
1681+ `schemas/${ serverName } /${ toolName } .signed.schema.json` ,
1682+ `schemas/${ serverName } .${ toolName } .signed.schema.json` ,
1683+ ]
1684+
1685+ for ( const relativePath of possiblePaths ) {
1686+ try {
1687+ // Check in project directory first if this is a project server
1688+ if ( source === "project" && vscode . workspace . workspaceFolders ?. length ) {
1689+ const projectPath = path . join ( vscode . workspace . workspaceFolders [ 0 ] . uri . fsPath , relativePath )
1690+ if ( await fileExistsAtPath ( projectPath ) ) {
1691+ return projectPath
1692+ }
1693+ }
1694+
1695+ // Check in global MCP directory
1696+ const provider = this . providerRef . deref ( )
1697+ if ( provider ) {
1698+ const globalPath = path . join ( await provider . ensureMcpServersDirectoryExists ( ) , relativePath )
1699+ if ( await fileExistsAtPath ( globalPath ) ) {
1700+ return globalPath
1701+ }
1702+ }
1703+ } catch ( error ) {
1704+ // Continue checking other paths
1705+ }
1706+ }
1707+
1708+ return null
1709+ }
1710+
1711+ /**
1712+ * Extract domain from server name for SchemaPin
1713+ */
1714+ private extractDomainFromServerName ( serverName : string ) : string {
1715+ // Try to extract domain from server name
1716+ const urlMatch = serverName . match ( / h t t p s ? : \/ \/ ( [ ^ \/ ] + ) / )
1717+ if ( urlMatch ) {
1718+ return urlMatch [ 1 ]
1719+ }
1720+
1721+ // Check if it looks like a domain
1722+ if ( serverName . includes ( "." ) && ! serverName . includes ( "/" ) ) {
1723+ return serverName
1724+ }
1725+
1726+ // Fallback to using the server name as domain
1727+ return serverName
1728+ }
1729+
15661730 async dispose ( ) : Promise < void > {
15671731 // Prevent multiple disposals
15681732 if ( this . isDisposed ) {
@@ -1578,6 +1742,11 @@ export class McpHub {
15781742 }
15791743 this . configChangeDebounceTimers . clear ( )
15801744
1745+ // Dispose SchemaPin service
1746+ if ( this . schemaPinService ) {
1747+ await this . schemaPinService . dispose ( )
1748+ }
1749+
15811750 this . removeAllFileWatchers ( )
15821751 for ( const connection of this . connections ) {
15831752 try {
0 commit comments