Skip to content

CETP Provider Integration Tutorial: Finance App Example

Walks through implementing a CETP v1 Provider in a finance app, exposing read-only portfolio data to ClawSeed.

1. ContentProvider Implementation

A single ContentProvider, ~100 lines of code:

class FinanceToolProvider : ContentProvider() {

    override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
        // Optional: verify caller identity (package name identification is only for
        // Provider's own authorization policy, not system-level trusted authentication)
        val callingUid = Binder.getCallingUid()
        val callerPackage = context!!.packageManager
            .getPackagesForUid(callingUid)?.firstOrNull()
        if (!isAuthorized(callerPackage)) {
            return errorBundle("PERMISSION_DENIED", "Unauthorized caller")
        }

        return when (method) {
            "list_tools" -> handleListTools()
            "execute_tool" -> handleExecuteTool(extras)
            "get_provider_info" -> handleGetProviderInfo()
            else -> errorBundle("TOOL_NOT_FOUND", "Unknown method: $method")
        }
    }

    private fun handleListTools(): Bundle {
        val data = """
        {
          "tools": [
            {
              "name": "get_portfolio_holdings",
              "description": "Get current portfolio holdings with symbol, quantity, cost basis, and current price",
              "parameters": {
                "type": "object",
                "properties": {
                  "account_type": {
                    "type": "string",
                    "enum": ["stock", "fund", "all"],
                    "description": "Filter by account type"
                  }
                },
                "required": []
              }
            },
            {
              "name": "get_transactions",
              "description": "Get transaction history",
              "parameters": {
                "type": "object",
                "properties": {
                  "start_date": {
                    "type": "string",
                    "description": "Start date in ISO 8601 format (YYYY-MM-DD)"
                  },
                  "end_date": {
                    "type": "string",
                    "description": "End date in ISO 8601 format (YYYY-MM-DD)"
                  },
                  "limit": {
                    "type": "integer",
                    "default": 50,
                    "description": "Maximum number of results"
                  }
                },
                "required": ["start_date"]
              }
            }
          ]
        }
        """.trimIndent()
        return successBundle(data)
    }

    private fun handleExecuteTool(extras: Bundle?): Bundle {
        val toolName = extras?.getString("tool_name")
            ?: return errorBundle("INVALID_ARGS", "missing tool_name")
        val args = extras.getString("args") ?: "{}"

        return when (toolName) {
            "get_portfolio_holdings" -> executeGetHoldings(args)
            "get_transactions" -> executeGetTransactions(args)
            else -> errorBundle("TOOL_NOT_FOUND", "Unknown tool: $toolName")
        }
    }

    private fun executeGetHoldings(argsJson: String): Bundle {
        val holdings = portfolioRepository.getHoldings()
        return successBundle(holdings.toJsonString())
    }

    private fun executeGetTransactions(argsJson: String): Bundle {
        val transactions = transactionRepository.getTransactions()
        return successBundle(transactions.toJsonString())
    }

    private fun handleGetProviderInfo(): Bundle {
        val data = """
        {
          "provider_name": "MyFinance",
          "description": "Personal finance data",
          "scopes": [
            {"name": "holdings", "description": "Portfolio holdings"},
            {"name": "transactions", "description": "Transaction history"}
          ]
        }
        """.trimIndent()
        return successBundle(data)
    }

    private fun successBundle(data: String): Bundle {
        return Bundle().apply {
            putString("status", "success")
            putString("data", data)
        }
    }

    private fun errorBundle(code: String, message: String): Bundle {
        return Bundle().apply {
            putString("status", "error")
            putString("error_code", code)
            putString("error_message", message)
        }
    }

    private fun isAuthorized(packageName: String?): Boolean {
        // Provider implements its own authorization strategy
        return true
    }

    // Required ContentProvider methods not used by CETP
    override fun onCreate() = true
    override fun query(u: Uri, p: Array<String>?, s: String?,
                       sa: Array<String>?, so: String?) = null
    override fun getType(uri: Uri) = null
    override fun insert(uri: Uri, values: ContentValues?) = null
    override fun delete(uri: Uri, s: String?, sa: Array<String>?) = 0
    override fun update(uri: Uri, v: ContentValues?,
                        s: String?, sa: Array<String>?) = 0
}

AUTH_REQUIRED Handling

When the user has not yet authorized access in the Provider app, return AUTH_REQUIRED:

private fun errorBundle(code: String, message: String,
                        resolutionHint: String? = null,
                        authorizeIntent: String? = null): Bundle {
    return Bundle().apply {
        putString("status", "error")
        putString("error_code", code)
        putString("error_message", message)
        resolutionHint?.let { putString("resolution_hint", it) }
        authorizeIntent?.let { putString("authorize_intent", it) }
    }
}

// Usage example
private fun executeGetHoldings(argsJson: String): Bundle {
    if (!userHasAuthorized()) {
        return errorBundle(
            "AUTH_REQUIRED",
            "User has not authorized this data scope",
            resolutionHint = "Please open the app to complete authorization",
            authorizeIntent = "com.example.finance.ACTION_AUTHORIZE_CLAWSEED"
        )
    }
    // ...normal data return
}

2. AndroidManifest Declaration

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.finance">

    <!-- Custom permission (for protocol entry identification and accidental call prevention only) -->
    <permission
        android:name="com.clawseed.permission.ACCESS_TOOLS"
        android:protectionLevel="normal"
        android:label="ClawSeed Tool Access"
        android:description="@string/permission_desc" />

    <application ...>

        <!-- Discovery entry point -->
        <service
            android:name=".clawseed.ToolProviderService"
            android:exported="true">
            <intent-filter>
                <action android:name="com.clawseed.action.TOOL_PROVIDER" />
            </intent-filter>
            <meta-data
                android:name="com.clawseed.tools.authority"
                android:value="com.example.finance.clawseed.tools" />
            <meta-data
                android:name="com.clawseed.tools.version"
                android:value="1" />
        </service>

        <!-- Data exchange -->
        <provider
            android:name=".clawseed.FinanceToolProvider"
            android:authorities="com.example.finance.clawseed.tools"
            android:exported="true"
            android:permission="com.clawseed.permission.ACCESS_TOOLS"
            android:initOrder="100" />

        <!-- Process keep-alive (recommended) -->
        <receiver
            android:name=".cetp.BootReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

    </application>
</manifest>

Some OEM ROMs (MIUI, ColorOS) may prevent ContentProvider processes from auto-starting, causing ContentResolver.call() to return Unknown authority.

BootReceiver

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // No action needed β€” simply triggering process start is sufficient.
        // ContentProvider.onCreate() is called automatically when the process starts.
    }
}

initOrder

Set android:initOrder="100" on the <provider> element to ensure early initialization (needed on some OEM ROMs).

4. Verification

After installing the Provider App, in ClawSeed: 1. Create a new session or reconnect 2. Check the registered tools list β€” finance__get_portfolio_holdings and similar tools should appear 3. Ask the Agent to invoke a tool in chat and verify the data is returned correctly 4. Test AUTH_REQUIRED flow: revoke authorization in the Provider App, invoke again and confirm the authorization prompt appears