1use im::HashMap as ImHashMap;
54use serde::{Deserialize, Serialize};
55use std::collections::HashMap;
56use truthlinked_core::pq_execution::{AccountId, CellCall, TransactionIntent};
57use truthlinked_governance::params as gp;
58use truthlinked_runtime::cells::{CellAccount, CellState};
59use truthlinked_runtime::compiler_aware::StorageKey;
60use truthlinked_runtime::types::{AccountRecord, CellUpdate, StateDiff};
61pub mod private_balance;
62pub mod zk_transfer;
63
64pub trait McpStateView {
65 fn cells(&self) -> &CellState;
66 fn accounts(&self) -> &ImHashMap<AccountId, AccountRecord>;
67}
68
69pub mod registry_keys {
77 pub const TOOL_COUNT: [u8; 32] = truthlinked_core::constants::MCP_REGISTRY_TOOL_COUNT_KEY;
79 pub const RESOURCE_COUNT: [u8; 32] =
81 truthlinked_core::constants::MCP_REGISTRY_RESOURCE_COUNT_KEY;
82 pub const PROMPT_COUNT: [u8; 32] = truthlinked_core::constants::MCP_REGISTRY_PROMPT_COUNT_KEY;
84 pub const REGISTRY_VER: [u8; 32] = truthlinked_core::constants::MCP_REGISTRY_VERSION_KEY;
86
87 pub fn tool_entry(index: u64) -> [u8; 32] {
89 let mut k = truthlinked_core::constants::mcp_key(b"mcp:tool:");
90 k[16..24].copy_from_slice(&index.to_le_bytes());
91 k
92 }
93 pub fn resource_entry(index: u64) -> [u8; 32] {
95 let mut k = truthlinked_core::constants::mcp_key(b"mcp:res:");
96 k[16..24].copy_from_slice(&index.to_le_bytes());
97 k
98 }
99 pub fn prompt_entry(index: u64) -> [u8; 32] {
101 let mut k = truthlinked_core::constants::mcp_key(b"mcp:prompt:");
102 k[16..24].copy_from_slice(&index.to_le_bytes());
103 k
104 }
105 pub fn name_to_tool(name: &str) -> [u8; 32] {
107 blake3_key(b"mcp:ntool:", name.as_bytes())
108 }
109 pub fn name_to_resource(name: &str) -> [u8; 32] {
111 blake3_key(b"mcp:nres:", name.as_bytes())
112 }
113 pub fn name_to_prompt(name: &str) -> [u8; 32] {
115 blake3_key(b"mcp:nprompt:", name.as_bytes())
116 }
117
118 pub fn key(prefix: &[u8]) -> [u8; 32] {
119 truthlinked_core::constants::mcp_key(prefix)
120 }
121 pub fn blake3_key(prefix: &[u8], data: &[u8]) -> [u8; 32] {
122 let mut input = prefix.to_vec();
123 input.extend_from_slice(data);
124 *blake3::hash(&input).as_bytes()
125 }
126}
127
128pub mod tool_keys {
130 pub const NAME: [u8; 32] = truthlinked_core::constants::MCP_TOOL_NAME_KEY;
132 pub const DESC_HASH: [u8; 32] = truthlinked_core::constants::MCP_TOOL_DESC_HASH_KEY;
134 pub const SCHEMA_HASH: [u8; 32] = truthlinked_core::constants::MCP_TOOL_SCHEMA_HASH_KEY;
136 pub const CATEGORY: [u8; 32] = truthlinked_core::constants::MCP_TOOL_CATEGORY_KEY;
138 pub const CALL_COUNT: [u8; 32] = truthlinked_core::constants::MCP_TOOL_CALL_COUNT_KEY;
140 pub const OWNER: [u8; 32] = truthlinked_core::constants::MCP_TOOL_OWNER_KEY;
142 pub const ENABLED: [u8; 32] = truthlinked_core::constants::MCP_TOOL_ENABLED_KEY;
144}
145
146pub mod resource_keys {
148 pub const NAME: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_NAME_KEY;
149 pub const URI_SCHEME: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_URI_SCHEME_KEY;
150 pub const MIME_TYPE: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_MIME_TYPE_KEY;
151 pub const CONTENT_HASH: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_CONTENT_HASH_KEY;
152 pub const UPDATED_AT: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_UPDATED_AT_KEY;
153 pub const READ_COUNT: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_READ_COUNT_KEY;
154
155 pub fn data_slot(slot_key: &[u8]) -> [u8; 32] {
157 super::registry_keys::blake3_key(b"res:data:", slot_key)
158 }
159}
160
161pub mod prompt_keys {
163 pub const NAME: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_NAME_KEY;
164 pub const TEMPLATE_HASH: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_TEMPLATE_HASH_KEY;
165 pub const ARG_COUNT: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_ARG_COUNT_KEY;
166 pub const USE_COUNT: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_USE_COUNT_KEY;
167 pub const APPROVED_AT: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_APPROVED_AT_KEY;
168
169 pub fn arg_schema(i: u8) -> [u8; 32] {
171 let mut k = truthlinked_core::constants::mcp_key(b"prompt:arg:");
172 k[16] = i;
173 k
174 }
175}
176
177pub mod policy_keys {
179 pub const OWNER: [u8; 32] = truthlinked_core::constants::MCP_POLICY_OWNER_KEY;
180 pub const STATUS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_STATUS_KEY;
181 pub const ALLOW_READS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ALLOW_READS_KEY;
183 pub const ALLOW_WRITES: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ALLOW_WRITES_KEY;
184 pub const ALLOW_ADMIN: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ALLOW_ADMIN_KEY;
185 pub const RATE_LIMIT: [u8; 32] = truthlinked_core::constants::MCP_POLICY_RATE_LIMIT_KEY;
186 pub const SPEND_PER_TX: [u8; 32] = truthlinked_core::constants::MCP_POLICY_SPEND_PER_TX_KEY;
188 pub const SPEND_EPOCH: [u8; 32] = truthlinked_core::constants::MCP_POLICY_SPEND_EPOCH_KEY;
190 pub const EPOCH_USED: [u8; 32] = truthlinked_core::constants::MCP_POLICY_EPOCH_USED_KEY;
191 pub const EPOCH_RESET_TS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_EPOCH_RESET_TS_KEY;
192 pub const ACTIONS_MIN: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ACTIONS_MIN_KEY;
193 pub const MIN_WINDOW_TS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_MIN_WINDOW_TS_KEY;
194 pub const TOTAL_ACTIONS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_TOTAL_ACTIONS_KEY;
195 pub const HITL_THRESHOLD: [u8; 32] = truthlinked_core::constants::MCP_POLICY_HITL_THRESHOLD_KEY;
196 pub const SUSPEND_REASON: [u8; 32] = truthlinked_core::constants::MCP_POLICY_SUSPEND_REASON_KEY;
197
198 pub fn tool_permission(tool_id: &[u8; 32]) -> [u8; 32] {
200 super::registry_keys::blake3_key(b"pol:tool:", tool_id)
201 }
202}
203
204pub mod agent_reg_keys {
206 pub const AGENT_COUNT: [u8; 32] = truthlinked_core::constants::MCP_AGENT_REGISTRY_COUNT_KEY;
207
208 pub fn agent_policy(agent_id: &[u8; 32]) -> [u8; 32] {
210 super::registry_keys::blake3_key(b"areg:pol:", agent_id)
211 }
212 pub fn agent_owner(agent_id: &[u8; 32]) -> [u8; 32] {
214 super::registry_keys::blake3_key(b"areg:own:", agent_id)
215 }
216 pub fn agent_entry(index: u64) -> [u8; 32] {
218 let mut bytes = [0u8; 8];
219 bytes.copy_from_slice(&index.to_le_bytes());
220 super::registry_keys::blake3_key(b"areg:idx:", &bytes)
221 }
222
223 pub fn agent_registered_at(agent_id: &[u8; 32]) -> [u8; 32] {
225 super::registry_keys::blake3_key(b"areg:reg:", agent_id)
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum McpIntent {
240 RegisterMcpTool {
244 tool_id: AccountId,
246 bytecode: Vec<u8>,
248 name: String,
250 input_schema_json: Vec<u8>,
252 category: u8,
254 declared_reads: Vec<[u8; 32]>,
256 declared_writes: Vec<[u8; 32]>,
257 commutative_keys: Vec<[u8; 32]>,
258 oracle_schema_ids: Vec<[u8; 32]>,
259 registry_id: AccountId,
261 },
262
263 RegisterMcpResource {
266 resource_id: AccountId,
267 bytecode: Vec<u8>, name: String,
269 uri_scheme: String, mime_type: String, initial_data: Vec<(Vec<u8>, Vec<u8>)>,
273 declared_reads: Vec<[u8; 32]>,
274 declared_writes: Vec<[u8; 32]>,
275 oracle_schema_ids: Vec<[u8; 32]>,
276 registry_id: AccountId,
277 },
278
279 RegisterMcpPrompt {
283 prompt_id: AccountId,
284 name: String,
285 template_bytes: Vec<u8>,
287 arguments: Vec<(String, String, bool)>,
289 registry_id: AccountId,
290 },
291
292 RegisterAgent {
296 agent_id: AccountId, policy_cell_id: AccountId,
298 agent_registry_id: AccountId,
299 },
300
301 UpdateAgentPolicy {
305 policy_cell_id: AccountId,
306 updates: PolicyUpdate,
307 },
308
309 SuspendAgent {
311 agent_id: AccountId,
312 agent_registry_id: AccountId,
313 reason: String,
314 },
315
316 ReinstateAgent {
318 agent_id: AccountId,
319 agent_registry_id: AccountId,
320 },
321
322 McpToolCall {
333 agent_id: AccountId,
334 tool_id: AccountId,
335 tool_calldata: Vec<u8>,
336 value: u128,
337 gas_limit: u64,
338 policy_cell_id: AccountId,
339 action_log_id: Option<AccountId>,
340 timestamp: u64,
341 },
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct PolicyUpdate {
347 pub status: Option<u8>, pub allow_reads: Option<bool>,
349 pub allow_writes: Option<bool>,
350 pub allow_admin: Option<bool>,
351 pub rate_limit: Option<u32>,
352 pub spend_per_tx: Option<u128>,
353 pub spend_epoch: Option<u128>,
354 pub hitl_threshold: Option<u128>,
355 pub tool_permissions: Vec<(AccountId, bool)>,
357}
358
359pub fn mcp_conflict_domain(intent: &McpIntent) -> (Vec<StorageKey>, Vec<StorageKey>) {
368 match intent {
369 McpIntent::RegisterMcpTool {
370 tool_id,
371 registry_id,
372 ..
373 } => {
374 (
377 vec![StorageKey::CellStorage(
378 *registry_id,
379 registry_keys::TOOL_COUNT,
380 )],
381 vec![
382 StorageKey::CellStorage(*registry_id, registry_keys::TOOL_COUNT),
383 StorageKey::CellStorage(*registry_id, registry_keys::REGISTRY_VER),
384 StorageKey::CellStorage(*tool_id, tool_keys::NAME),
385 ],
386 )
387 }
388
389 McpIntent::RegisterMcpResource {
390 resource_id,
391 registry_id,
392 ..
393 } => (
394 vec![StorageKey::CellStorage(
395 *registry_id,
396 registry_keys::RESOURCE_COUNT,
397 )],
398 vec![
399 StorageKey::CellStorage(*registry_id, registry_keys::RESOURCE_COUNT),
400 StorageKey::CellStorage(*registry_id, registry_keys::REGISTRY_VER),
401 StorageKey::CellStorage(*resource_id, resource_keys::NAME),
402 ],
403 ),
404
405 McpIntent::RegisterMcpPrompt {
406 prompt_id,
407 registry_id,
408 ..
409 } => (
410 vec![StorageKey::CellStorage(
411 *registry_id,
412 registry_keys::PROMPT_COUNT,
413 )],
414 vec![
415 StorageKey::CellStorage(*registry_id, registry_keys::PROMPT_COUNT),
416 StorageKey::CellStorage(*registry_id, registry_keys::REGISTRY_VER),
417 StorageKey::CellStorage(*prompt_id, prompt_keys::NAME),
418 ],
419 ),
420
421 McpIntent::RegisterAgent {
422 agent_id,
423 agent_registry_id,
424 ..
425 } => (
426 vec![StorageKey::CellStorage(
427 *agent_registry_id,
428 agent_reg_keys::AGENT_COUNT,
429 )],
430 vec![
431 StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::agent_policy(agent_id)),
432 StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::agent_owner(agent_id)),
433 StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::agent_entry(0)),
434 StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::AGENT_COUNT),
435 ],
436 ),
437
438 McpIntent::SuspendAgent {
439 agent_id,
440 agent_registry_id,
441 ..
442 }
443 | McpIntent::ReinstateAgent {
444 agent_id,
445 agent_registry_id,
446 } => (
447 vec![],
448 vec![StorageKey::CellStorage(
449 *agent_registry_id,
450 agent_reg_keys::agent_policy(agent_id),
451 )],
452 ),
453
454 McpIntent::UpdateAgentPolicy {
455 policy_cell_id,
456 updates,
457 } => {
458 let mut writes = vec![
459 StorageKey::CellStorage(*policy_cell_id, policy_keys::STATUS),
460 StorageKey::CellStorage(*policy_cell_id, policy_keys::ALLOW_READS),
461 StorageKey::CellStorage(*policy_cell_id, policy_keys::ALLOW_WRITES),
462 ];
463 for (tool_id, _) in &updates.tool_permissions {
464 writes.push(StorageKey::CellStorage(
465 *policy_cell_id,
466 policy_keys::tool_permission(tool_id),
467 ));
468 }
469 (vec![], writes)
470 }
471
472 McpIntent::McpToolCall {
473 agent_id,
474 tool_id,
475 policy_cell_id,
476 action_log_id,
477 ..
478 } => {
479 let reads = vec![
487 StorageKey::CellStorage(*policy_cell_id, policy_keys::STATUS),
488 StorageKey::CellStorage(*policy_cell_id, policy_keys::ALLOW_WRITES),
489 StorageKey::CellStorage(*policy_cell_id, policy_keys::SPEND_EPOCH),
490 ];
491 let mut writes = vec![
492 StorageKey::CellStorage(*policy_cell_id, policy_keys::TOTAL_ACTIONS),
494 StorageKey::CellStorage(*policy_cell_id, policy_keys::ACTIONS_MIN),
495 StorageKey::CellStorage(*tool_id, tool_keys::CALL_COUNT),
496 ];
497 if let Some(log_id) = action_log_id {
498 writes.push(StorageKey::CellStorage(
500 *log_id,
501 registry_keys::blake3_key(b"log:", agent_id),
502 ));
503 }
504 (reads, writes)
505 }
506 }
507}
508
509pub fn diff_register_tool(
523 state: &impl McpStateView,
524 sender: AccountId,
525 intent: &McpIntent,
526 timestamp: u64,
527) -> Result<StateDiff, String> {
528 let (
529 tool_id,
530 bytecode,
531 name,
532 input_schema_json,
533 category,
534 declared_reads,
535 declared_writes,
536 commutative_keys,
537 oracle_schema_ids,
538 registry_id,
539 ) = match intent {
540 McpIntent::RegisterMcpTool {
541 tool_id,
542 bytecode,
543 name,
544 input_schema_json,
545 category,
546 declared_reads,
547 declared_writes,
548 commutative_keys,
549 oracle_schema_ids,
550 registry_id,
551 } => (
552 tool_id,
553 bytecode,
554 name,
555 input_schema_json,
556 category,
557 declared_reads,
558 declared_writes,
559 commutative_keys,
560 oracle_schema_ids,
561 registry_id,
562 ),
563 _ => return Err("Wrong intent".into()),
564 };
565
566 if name.len() > 64 {
567 return Err("Tool name too long (max 64 bytes)".into());
568 }
569 if bytecode.is_empty() {
570 return Err("Tool bytecode is empty".into());
571 }
572 if bytecode.len() > gp::get_usize(gp::PARAM_MAX_CELL_BYTECODE_SIZE) {
573 return Err("Tool bytecode exceeds max size".into());
574 }
575 if bytecode.len() < 4 || &bytecode[0..4] != b"AXIO" {
576 return Err("Invalid Axiom bytecode: missing magic bytes".into());
577 }
578 truthlinked_runtime::cells::CellAccount::verify_manifest_against_bytecode(
579 bytecode,
580 declared_reads,
581 declared_writes,
582 &[],
583 )?;
584 truthlinked_runtime::cells::CellAccount::require_inferable(bytecode, &[])?;
585
586 if state.cells().cells.contains_key(tool_id) {
588 return Err(format!(
589 "Tool cell {} already deployed",
590 hex::encode(tool_id)
591 ));
592 }
593
594 let registry = state
596 .cells()
597 .cells
598 .get(registry_id)
599 .ok_or("McpRegistry cell not found")?;
600
601 let rent_deposit = gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE);
603 let sender_account = state
604 .accounts()
605 .get(&sender)
606 .ok_or("Sender account not found")?;
607 if sender_account.balance < rent_deposit {
608 return Err("Insufficient balance for rent deposit".into());
609 }
610
611 let mut tool_storage: HashMap<[u8; 32], [u8; 32]> = HashMap::new();
613
614 let mut name_bytes = [0u8; 32];
616 let n = name.len().min(32);
617 name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
618 tool_storage.insert(tool_keys::NAME, name_bytes);
619
620 let schema_hash = *blake3::hash(input_schema_json).as_bytes();
622 tool_storage.insert(tool_keys::SCHEMA_HASH, schema_hash);
623
624 tool_storage.insert(tool_keys::DESC_HASH, schema_hash);
626
627 let mut cat_bytes = [0u8; 32];
629 cat_bytes[0] = *category;
630 tool_storage.insert(tool_keys::CATEGORY, cat_bytes);
631
632 let mut enabled = [0u8; 32];
634 enabled[0] = 1;
635 tool_storage.insert(tool_keys::ENABLED, enabled);
636
637 tool_storage.insert(tool_keys::OWNER, sender);
639
640 tool_storage.insert(tool_keys::CALL_COUNT, [0u8; 32]);
642
643 let manifest_hash = truthlinked_runtime::cells::CellAccount::compute_manifest_hash(
645 bytecode,
646 declared_reads,
647 declared_writes,
648 commutative_keys,
649 oracle_schema_ids,
650 );
651
652 let tool_cell = truthlinked_runtime::cells::CellAccount {
654 cell_id: *tool_id,
655 owner: truthlinked_core::pq_execution::system_authority_id(),
656 bytecode: bytecode.clone(),
657 storage: tool_storage,
658 balance: 0,
659 rent_deposit,
660 is_token: false,
661 token_config: None,
662 created_at: timestamp,
663 upgraded_at: None,
664 last_rent_paid_height: 0,
665 rent_grace_blocks: gp::get_u64(gp::PARAM_STORAGE_RENT_GRACE_PERIOD_BLOCKS),
666 pending_owner: None,
667 is_immutable: false,
668 declared_reads: declared_reads.clone(),
669 declared_writes: declared_writes.clone(),
670 commutative_keys: commutative_keys.clone(),
671 storage_key_specs: vec![],
672 oracle_schema_ids: oracle_schema_ids.clone(),
673 governance_proposal: None,
674 manifest_version: 1,
675 manifest_hash,
676 };
677
678 let current_count = read_u64_from_storage(registry, ®istry_keys::TOOL_COUNT);
680 let new_count = current_count.checked_add(1).ok_or("Tool count overflow")?;
681
682 let mut diff = StateDiff::default();
683
684 let mut sender_updated = sender_account.clone();
685 sender_updated.balance = sender_updated.balance.saturating_sub(rent_deposit);
686 diff.account_updates.insert(sender, sender_updated);
687
688 diff.cell_updates.push(CellUpdate::Deploy {
690 cell_id: *tool_id,
691 cell: tool_cell,
692 });
693
694 diff.cell_updates.push(CellUpdate::StorageChange {
696 cell_id: *registry_id,
697 storage_diff: {
698 let mut m = HashMap::new();
699 let mut count_bytes = [0u8; 32];
701 count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
702 m.insert(registry_keys::TOOL_COUNT, Some(count_bytes));
703 m.insert(registry_keys::tool_entry(current_count), Some(*tool_id));
705 m.insert(registry_keys::name_to_tool(name), Some(*tool_id));
707 let ver = read_u64_from_storage(registry, ®istry_keys::REGISTRY_VER);
709 let mut ver_bytes = [0u8; 32];
710 ver_bytes[..8].copy_from_slice(&(ver + 1).to_le_bytes());
711 m.insert(registry_keys::REGISTRY_VER, Some(ver_bytes));
712 m
713 },
714 });
715
716 diff.cu_fee = gp::get_u64(gp::PARAM_GAS_DEPLOY_CELL) as u128;
717
718 Ok(diff)
719}
720
721pub fn diff_register_agent(
724 state: &impl McpStateView,
725 sender: AccountId,
726 intent: &McpIntent,
727 timestamp: u64,
728) -> Result<StateDiff, String> {
729 let (agent_id, policy_cell_id, agent_registry_id) = match intent {
730 McpIntent::RegisterAgent {
731 agent_id,
732 policy_cell_id,
733 agent_registry_id,
734 } => (agent_id, policy_cell_id, agent_registry_id),
735 _ => return Err("Wrong intent".into()),
736 };
737
738 let registry = state
740 .cells()
741 .cells
742 .get(agent_registry_id)
743 .ok_or("AgentRegistry cell not found")?;
744
745 if !state.cells().cells.contains_key(policy_cell_id) {
747 return Err("Policy cell not found".into());
748 }
749
750 if &sender == agent_id {
753 return Err(
754 "Agent cannot register itself: owner and agent_id must be different accounts".into(),
755 );
756 }
757
758 let existing_policy_slot = agent_reg_keys::agent_policy(agent_id);
760 if registry.storage.get(&existing_policy_slot) != Some(&[0u8; 32])
761 && registry.storage.contains_key(&existing_policy_slot)
762 {
763 return Err("Agent already registered".into());
764 }
765
766 let current_count = read_u64_from_storage(registry, &agent_reg_keys::AGENT_COUNT);
767 let new_count = current_count.checked_add(1).ok_or("Agent count overflow")?;
768
769 let mut diff = StateDiff::default();
770 diff.cell_updates.push(CellUpdate::StorageChange {
771 cell_id: *agent_registry_id,
772 storage_diff: {
773 let mut m = HashMap::new();
774 m.insert(
775 agent_reg_keys::agent_policy(agent_id),
776 Some(*policy_cell_id),
777 );
778 m.insert(agent_reg_keys::agent_owner(agent_id), Some(sender));
779 m.insert(agent_reg_keys::agent_entry(current_count), Some(*agent_id));
780 let mut ts_bytes = [0u8; 32];
781 ts_bytes[..8].copy_from_slice(×tamp.to_le_bytes());
782 m.insert(
783 agent_reg_keys::agent_registered_at(agent_id),
784 Some(ts_bytes),
785 );
786 let mut count_bytes = [0u8; 32];
787 count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
788 m.insert(agent_reg_keys::AGENT_COUNT, Some(count_bytes));
789 m
790 },
791 });
792
793 diff.cell_updates.push(CellUpdate::StorageChange {
797 cell_id: *policy_cell_id,
798 storage_diff: {
799 let mut m = HashMap::new();
800 m.insert(policy_keys::STATUS, Some([0u8; 32]));
802 m.insert(policy_keys::OWNER, Some(sender));
804 let mut allow_reads = [0u8; 32];
806 allow_reads[0] = 1;
807 m.insert(policy_keys::ALLOW_READS, Some(allow_reads));
808 let mut allow_writes = [0u8; 32];
810 allow_writes[0] = 1;
811 m.insert(policy_keys::ALLOW_WRITES, Some(allow_writes));
812 m.insert(policy_keys::ALLOW_ADMIN, Some([0u8; 32]));
814 m.insert(policy_keys::RATE_LIMIT, Some([0u8; 32]));
816 m.insert(policy_keys::SPEND_PER_TX, Some([0u8; 32]));
818 m.insert(policy_keys::SPEND_EPOCH, Some([0u8; 32]));
820 m.insert(policy_keys::TOTAL_ACTIONS, Some([0u8; 32]));
822 m
823 },
824 });
825
826 diff.cu_fee = gp::get_u64(gp::PARAM_GAS_TRANSFER) as u128;
827 Ok(diff)
828}
829
830pub fn diff_set_agent_status(
832 state: &impl McpStateView,
833 sender: AccountId,
834 intent: &McpIntent,
835) -> Result<StateDiff, String> {
836 let (agent_id, registry_id, new_status, reason) = match intent {
837 McpIntent::SuspendAgent {
838 agent_id,
839 agent_registry_id,
840 reason,
841 } => (agent_id, agent_registry_id, 1u8, reason.as_str()),
842 McpIntent::ReinstateAgent {
843 agent_id,
844 agent_registry_id,
845 } => (agent_id, agent_registry_id, 0u8, ""),
846 _ => return Err("Wrong intent".into()),
847 };
848
849 let registry = state
850 .cells()
851 .cells
852 .get(registry_id)
853 .ok_or("AgentRegistry not found")?;
854
855 let owner_slot = agent_reg_keys::agent_owner(agent_id);
857 let stored_owner = registry
858 .storage
859 .get(&owner_slot)
860 .copied()
861 .unwrap_or([0u8; 32]);
862 if stored_owner != sender {
863 return Err("Only the agent's registered owner can change agent status".into());
864 }
865
866 let policy_id_bytes = registry
868 .storage
869 .get(&agent_reg_keys::agent_policy(agent_id))
870 .copied()
871 .ok_or("Agent not registered")?;
872
873 let mut diff = StateDiff::default();
874
875 let mut status_bytes = [0u8; 32];
877 status_bytes[0] = new_status;
878 diff.cell_updates.push(CellUpdate::StorageChange {
879 cell_id: policy_id_bytes,
880 storage_diff: {
881 let mut m = HashMap::new();
882 m.insert(policy_keys::STATUS, Some(status_bytes));
883 if !reason.is_empty() {
884 let reason_hash = *blake3::hash(reason.as_bytes()).as_bytes();
886 m.insert(policy_keys::SUSPEND_REASON, Some(reason_hash));
887 }
888 m
889 },
890 });
891
892 diff.cu_fee = gp::get_u64(gp::PARAM_GAS_TRANSFER) as u128;
893 Ok(diff)
894}
895
896pub fn compile_tool_call_to_chain(intent: &McpIntent) -> Result<TransactionIntent, String> {
919 let (
920 agent_id,
921 tool_id,
922 tool_calldata,
923 value,
924 gas_limit,
925 policy_cell_id,
926 action_log_id,
927 timestamp,
928 ) = match intent {
929 McpIntent::McpToolCall {
930 agent_id,
931 tool_id,
932 tool_calldata,
933 value,
934 gas_limit,
935 policy_cell_id,
936 action_log_id,
937 timestamp,
938 } => (
939 agent_id,
940 tool_id,
941 tool_calldata,
942 value,
943 gas_limit,
944 policy_cell_id,
945 action_log_id,
946 timestamp,
947 ),
948 _ => return Err("Wrong intent type".into()),
949 };
950 let log_id = action_log_id
951 .as_ref()
952 .ok_or("McpToolCall requires action_log_id")?;
953
954 let calldata_hash = *blake3::hash(tool_calldata).as_bytes();
955 let mut policy_calldata = Vec::with_capacity(120);
956 policy_calldata.extend_from_slice(agent_id);
957 policy_calldata.extend_from_slice(tool_id);
958 policy_calldata.extend_from_slice(&calldata_hash);
959 policy_calldata.extend_from_slice(&value.to_le_bytes());
960 policy_calldata.extend_from_slice(×tamp.to_le_bytes());
961
962 let mut calls = vec![
963 CellCall {
964 cell_id: *policy_cell_id,
965 calldata: policy_calldata,
966 value: 0,
967 use_result_from: None,
968 },
969 CellCall {
971 cell_id: *tool_id,
972 calldata: tool_calldata.clone(),
973 value: *value,
974 use_result_from: None, },
976 ];
977
978 let mut log_calldata = Vec::with_capacity(96);
979 log_calldata.extend_from_slice(agent_id);
980 log_calldata.extend_from_slice(tool_id);
981 log_calldata.extend_from_slice(×tamp.to_le_bytes());
982
983 calls.push(CellCall {
984 cell_id: *log_id,
985 calldata: log_calldata,
986 value: 0,
987 use_result_from: Some(1),
988 });
989
990 Ok(TransactionIntent::CallCellChain {
991 calls,
992 gas_limit: *gas_limit,
993 })
994}
995
996pub fn diff_register_resource(
999 state: &impl McpStateView,
1000 sender: AccountId,
1001 intent: &McpIntent,
1002 timestamp: u64,
1003) -> Result<StateDiff, String> {
1004 let (
1005 resource_id,
1006 bytecode,
1007 name,
1008 uri_scheme,
1009 mime_type,
1010 initial_data,
1011 declared_reads,
1012 declared_writes,
1013 oracle_schema_ids,
1014 registry_id,
1015 ) = match intent {
1016 McpIntent::RegisterMcpResource {
1017 resource_id,
1018 bytecode,
1019 name,
1020 uri_scheme,
1021 mime_type,
1022 initial_data,
1023 declared_reads,
1024 declared_writes,
1025 oracle_schema_ids,
1026 registry_id,
1027 } => (
1028 resource_id,
1029 bytecode,
1030 name,
1031 uri_scheme,
1032 mime_type,
1033 initial_data,
1034 declared_reads,
1035 declared_writes,
1036 oracle_schema_ids,
1037 registry_id,
1038 ),
1039 _ => return Err("Wrong intent".into()),
1040 };
1041
1042 if name.len() > 64 {
1043 return Err("Resource name too long".into());
1044 }
1045 if uri_scheme.len() > 64 {
1046 return Err("URI scheme too long".into());
1047 }
1048 if !bytecode.is_empty() {
1049 if bytecode.len() > gp::get_usize(gp::PARAM_MAX_CELL_BYTECODE_SIZE) {
1050 return Err("Resource bytecode exceeds max size".into());
1051 }
1052 if bytecode.len() < 4 || &bytecode[0..4] != b"AXIO" {
1053 return Err("Invalid Axiom bytecode: missing magic bytes".into());
1054 }
1055 truthlinked_runtime::cells::CellAccount::verify_manifest_against_bytecode(
1056 bytecode,
1057 declared_reads,
1058 declared_writes,
1059 &[],
1060 )?;
1061 truthlinked_runtime::cells::CellAccount::require_inferable(bytecode, &[])?;
1062 }
1063
1064 if state.cells().cells.contains_key(resource_id) {
1065 return Err(format!(
1066 "Resource cell {} already deployed",
1067 hex::encode(resource_id)
1068 ));
1069 }
1070
1071 let registry = state
1072 .cells()
1073 .cells
1074 .get(registry_id)
1075 .ok_or("McpRegistry cell not found")?;
1076
1077 let rent_deposit = gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE);
1079 let sender_account = state
1080 .accounts()
1081 .get(&sender)
1082 .ok_or("Sender account not found")?;
1083 if sender_account.balance < rent_deposit {
1084 return Err("Insufficient balance for rent deposit".into());
1085 }
1086
1087 let mut resource_storage: HashMap<[u8; 32], [u8; 32]> = HashMap::new();
1089
1090 let mut name_bytes = [0u8; 32];
1091 let n = name.len().min(32);
1092 name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
1093 resource_storage.insert(resource_keys::NAME, name_bytes);
1094
1095 let mut uri_bytes = [0u8; 32];
1096 let u = uri_scheme.len().min(32);
1097 uri_bytes[..u].copy_from_slice(&uri_scheme.as_bytes()[..u]);
1098 resource_storage.insert(resource_keys::URI_SCHEME, uri_bytes);
1099
1100 let mut mime_bytes = [0u8; 32];
1101 let m = mime_type.len().min(32);
1102 mime_bytes[..m].copy_from_slice(&mime_type.as_bytes()[..m]);
1103 resource_storage.insert(resource_keys::MIME_TYPE, mime_bytes);
1104
1105 let mut ts_bytes = [0u8; 32];
1107 ts_bytes[..8].copy_from_slice(×tamp.to_le_bytes());
1108 resource_storage.insert(resource_keys::UPDATED_AT, ts_bytes);
1109
1110 for (slot_key, content) in initial_data {
1112 if content.len() > 32 {
1113 return Err("Resource initial data values must be <= 32 bytes".into());
1114 }
1115 let storage_key = resource_keys::data_slot(slot_key);
1116 let mut value = [0u8; 32];
1117 let len = content.len().min(32);
1118 value[..len].copy_from_slice(&content[..len]);
1119 resource_storage.insert(storage_key, value);
1120 }
1121
1122 let all_data: Vec<u8> = initial_data
1124 .iter()
1125 .flat_map(|(k, v)| {
1126 let mut combined = k.clone();
1127 combined.extend_from_slice(v);
1128 combined
1129 })
1130 .collect();
1131 resource_storage.insert(
1132 resource_keys::CONTENT_HASH,
1133 *blake3::hash(&all_data).as_bytes(),
1134 );
1135
1136 let manifest_hash = truthlinked_runtime::cells::CellAccount::compute_manifest_hash(
1137 bytecode,
1138 declared_reads,
1139 declared_writes,
1140 &[],
1141 oracle_schema_ids,
1142 );
1143
1144 let resource_cell = truthlinked_runtime::cells::CellAccount {
1145 cell_id: *resource_id,
1146 owner: truthlinked_core::pq_execution::system_authority_id(),
1147 bytecode: bytecode.clone(),
1148 storage: resource_storage,
1149 balance: 0,
1150 rent_deposit,
1151 is_token: false,
1152 token_config: None,
1153 created_at: timestamp,
1154 upgraded_at: None,
1155 last_rent_paid_height: 0,
1156 rent_grace_blocks: gp::get_u64(gp::PARAM_STORAGE_RENT_GRACE_PERIOD_BLOCKS),
1157 pending_owner: None,
1158 is_immutable: false,
1159 declared_reads: declared_reads.clone(),
1160 declared_writes: declared_writes.clone(),
1161 commutative_keys: vec![],
1162 storage_key_specs: vec![],
1163 oracle_schema_ids: oracle_schema_ids.clone(),
1164 governance_proposal: None,
1165 manifest_version: 1,
1166 manifest_hash,
1167 };
1168
1169 let current_count = read_u64_from_storage(registry, ®istry_keys::RESOURCE_COUNT);
1170 let new_count = current_count
1171 .checked_add(1)
1172 .ok_or("Resource count overflow")?;
1173
1174 let mut diff = StateDiff::default();
1175
1176 let mut sender_updated = sender_account.clone();
1177 sender_updated.balance = sender_updated.balance.saturating_sub(rent_deposit);
1178 diff.account_updates.insert(sender, sender_updated);
1179
1180 diff.cell_updates.push(CellUpdate::Deploy {
1181 cell_id: *resource_id,
1182 cell: resource_cell,
1183 });
1184
1185 diff.cell_updates.push(CellUpdate::StorageChange {
1186 cell_id: *registry_id,
1187 storage_diff: {
1188 let mut m = HashMap::new();
1189 let mut count_bytes = [0u8; 32];
1190 count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
1191 m.insert(registry_keys::RESOURCE_COUNT, Some(count_bytes));
1192 m.insert(
1193 registry_keys::resource_entry(current_count),
1194 Some(*resource_id),
1195 );
1196 m.insert(registry_keys::name_to_resource(name), Some(*resource_id));
1197 let ver = read_u64_from_storage(registry, ®istry_keys::REGISTRY_VER);
1198 let mut ver_bytes = [0u8; 32];
1199 ver_bytes[..8].copy_from_slice(&(ver + 1).to_le_bytes());
1200 m.insert(registry_keys::REGISTRY_VER, Some(ver_bytes));
1201 m
1202 },
1203 });
1204
1205 diff.cu_fee = gp::get_u64(gp::PARAM_GAS_DEPLOY_CELL) as u128;
1206 Ok(diff)
1207}
1208
1209pub fn diff_register_prompt(
1213 state: &impl McpStateView,
1214 sender: AccountId,
1215 intent: &McpIntent,
1216 timestamp: u64,
1217) -> Result<StateDiff, String> {
1218 let (prompt_id, name, template_bytes, arguments, registry_id) = match intent {
1219 McpIntent::RegisterMcpPrompt {
1220 prompt_id,
1221 name,
1222 template_bytes,
1223 arguments,
1224 registry_id,
1225 } => (prompt_id, name, template_bytes, arguments, registry_id),
1226 _ => return Err("Wrong intent".into()),
1227 };
1228
1229 if name.len() > 64 {
1230 return Err("Prompt name too long".into());
1231 }
1232 if arguments.len() > 255 {
1233 return Err("Too many arguments (max 255)".into());
1234 }
1235
1236 if state.cells().cells.contains_key(prompt_id) {
1237 return Err(format!(
1238 "Prompt cell {} already deployed",
1239 hex::encode(prompt_id)
1240 ));
1241 }
1242
1243 let registry = state
1244 .cells()
1245 .cells
1246 .get(registry_id)
1247 .ok_or("McpRegistry cell not found")?;
1248
1249 let rent_deposit = gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE);
1251 let sender_account = state
1252 .accounts()
1253 .get(&sender)
1254 .ok_or("Sender account not found")?;
1255 if sender_account.balance < rent_deposit {
1256 return Err("Insufficient balance for rent deposit".into());
1257 }
1258
1259 let mut prompt_storage: HashMap<[u8; 32], [u8; 32]> = HashMap::new();
1260
1261 let mut name_bytes = [0u8; 32];
1262 let n = name.len().min(32);
1263 name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
1264 prompt_storage.insert(prompt_keys::NAME, name_bytes);
1265
1266 let template_hash = *blake3::hash(template_bytes).as_bytes();
1268 prompt_storage.insert(prompt_keys::TEMPLATE_HASH, template_hash);
1269
1270 let mut argc_bytes = [0u8; 32];
1272 argc_bytes[0] = arguments.len() as u8;
1273 prompt_storage.insert(prompt_keys::ARG_COUNT, argc_bytes);
1274
1275 for (i, (arg_name, _desc, _required)) in arguments.iter().enumerate() {
1277 let mut arg_bytes = [0u8; 32];
1278 let n = arg_name.len().min(32);
1279 arg_bytes[..n].copy_from_slice(&arg_name.as_bytes()[..n]);
1280 prompt_storage.insert(prompt_keys::arg_schema(i as u8), arg_bytes);
1281 }
1282
1283 prompt_storage.insert(prompt_keys::APPROVED_AT, [0u8; 32]);
1285 prompt_storage.insert(prompt_keys::USE_COUNT, [0u8; 32]);
1286
1287 let manifest_hash =
1288 truthlinked_runtime::cells::CellAccount::compute_manifest_hash(&[], &[], &[], &[], &[]);
1289
1290 let prompt_cell = truthlinked_runtime::cells::CellAccount {
1291 cell_id: *prompt_id,
1292 owner: truthlinked_core::pq_execution::system_authority_id(),
1293 bytecode: vec![], storage: prompt_storage,
1295 balance: 0,
1296 rent_deposit,
1297 is_token: false,
1298 token_config: None,
1299 created_at: timestamp,
1300 upgraded_at: None,
1301 last_rent_paid_height: 0,
1302 rent_grace_blocks: gp::get_u64(gp::PARAM_STORAGE_RENT_GRACE_PERIOD_BLOCKS),
1303 pending_owner: None,
1304 is_immutable: false,
1305 declared_reads: vec![],
1306 declared_writes: vec![],
1307 commutative_keys: vec![prompt_keys::USE_COUNT], storage_key_specs: vec![],
1309 oracle_schema_ids: vec![],
1310 governance_proposal: None,
1311 manifest_version: 1,
1312 manifest_hash,
1313 };
1314
1315 let current_count = read_u64_from_storage(registry, ®istry_keys::PROMPT_COUNT);
1316 let new_count = current_count
1317 .checked_add(1)
1318 .ok_or("Prompt count overflow")?;
1319
1320 let mut diff = StateDiff::default();
1321
1322 let mut sender_updated = sender_account.clone();
1323 sender_updated.balance = sender_updated.balance.saturating_sub(rent_deposit);
1324 diff.account_updates.insert(sender, sender_updated);
1325
1326 diff.cell_updates.push(CellUpdate::Deploy {
1327 cell_id: *prompt_id,
1328 cell: prompt_cell,
1329 });
1330
1331 diff.cell_updates.push(CellUpdate::StorageChange {
1332 cell_id: *registry_id,
1333 storage_diff: {
1334 let mut m = HashMap::new();
1335 let mut count_bytes = [0u8; 32];
1336 count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
1337 m.insert(registry_keys::PROMPT_COUNT, Some(count_bytes));
1338 m.insert(registry_keys::prompt_entry(current_count), Some(*prompt_id));
1339 m.insert(registry_keys::name_to_prompt(name), Some(*prompt_id));
1340 let ver = read_u64_from_storage(registry, ®istry_keys::REGISTRY_VER);
1341 let mut ver_bytes = [0u8; 32];
1342 ver_bytes[..8].copy_from_slice(&(ver + 1).to_le_bytes());
1343 m.insert(registry_keys::REGISTRY_VER, Some(ver_bytes));
1344 m
1345 },
1346 });
1347
1348 diff.cu_fee = gp::get_u64(gp::PARAM_GAS_DEPLOY_CELL) as u128;
1352 Ok(diff)
1353}
1354
1355fn is_native_mcp_tool(name: &str) -> bool {
1370 matches!(
1371 name,
1372 "get_chain_info"
1373 | "get_balance"
1374 | "get_validators"
1375 | "get_token_info"
1376 | "get_cell_info"
1377 | "get_transaction"
1378 | "get_staking_info"
1379 | "get_oracle_result"
1380 | "get_account_history"
1381 | "submit_transaction"
1382 | "http_fetch"
1383 | "get_sdk"
1384 | "faucet"
1385 )
1386}
1387
1388pub fn enumerate_tools(
1389 state: &impl McpStateView,
1390 registry_id: &AccountId,
1391 agent_reg_id: &AccountId,
1392 agent_id: &AccountId,
1393) -> Vec<serde_json::Value> {
1394 let registry = match state.cells().cells.get(registry_id) {
1395 Some(r) => r,
1396 None => return vec![],
1397 };
1398
1399 let tool_count = read_u64_from_storage(registry, ®istry_keys::TOOL_COUNT);
1400
1401 let policy_cell = state.cells().cells.get(agent_reg_id).and_then(|ar| {
1403 let pol_id = ar
1404 .storage
1405 .get(&agent_reg_keys::agent_policy(agent_id))
1406 .copied()?;
1407 state.cells().cells.get(&pol_id)
1408 });
1409
1410 (0..tool_count).filter_map(|i| {
1411 let tool_id = registry.storage.get(®istry_keys::tool_entry(i)).copied()?;
1412 let tool = state.cells().cells.get(&tool_id)?;
1413
1414 let enabled = tool.storage.get(&tool_keys::ENABLED)
1416 .map(|b| b[0] == 1).unwrap_or(false);
1417 if !enabled { return None; }
1418
1419 if let Some(policy) = policy_cell {
1421 let perm = policy.storage.get(&policy_keys::tool_permission(&tool_id))
1422 .map(|b| b[0] == 1)
1423 .unwrap_or(false);
1424 let allow_reads = policy.storage.get(&policy_keys::ALLOW_READS)
1426 .map(|b| b[0] == 1).unwrap_or(false);
1427 let category = tool.storage.get(&tool_keys::CATEGORY)
1428 .map(|b| b[0]).unwrap_or(0);
1429 if !perm && !(allow_reads && category == 0) {
1430 return None;
1431 }
1432 }
1433
1434 let name_bytes = tool.storage.get(&tool_keys::NAME).copied().unwrap_or([0u8; 32]);
1435 let name = String::from_utf8_lossy(name_bytes.split(|&b| b == 0).next().unwrap_or(&[]))
1436 .to_string();
1437
1438 if !is_native_mcp_tool(&name) {
1439 return None;
1440 }
1441
1442 let builtin_schemas: std::collections::HashMap<&str, serde_json::Value> = [
1443 ("get_chain_info", serde_json::json!({"type":"object","properties":{},"required":[]})),
1444 ("get_balance", serde_json::json!({"type":"object","properties":{"account_id":{"type":"string","description":"32-byte account ID as hex"}},"required":["account_id"]})),
1445 ("get_validators", serde_json::json!({"type":"object","properties":{},"required":[]})),
1446 ("get_token_info", serde_json::json!({"type":"object","properties":{},"required":[]})),
1447 ("get_cell_info", serde_json::json!({"type":"object","properties":{"cell_id":{"type":"string","description":"32-byte cell ID as hex"}},"required":["cell_id"]})),
1448 ("get_transaction", serde_json::json!({"type":"object","properties":{"tx_hash":{"type":"string","description":"32-byte tx hash as hex"}},"required":["tx_hash"]})),
1449 ("get_staking_info", serde_json::json!({"type":"object","properties":{},"required":[]})),
1450 ("get_oracle_result", serde_json::json!({"type":"object","properties":{"request_id":{"type":"string","description":"32-byte oracle request ID as hex"}},"required":["request_id"]})),
1451 ("get_account_history", serde_json::json!({"type":"object","properties":{"account_id":{"type":"string"},"limit":{"type":"integer","default":20}},"required":["account_id"]})),
1452 ("submit_transaction", serde_json::json!({"type":"object","properties":{"tx_hex":{"type":"string","description":"Hex-encoded bincode-serialized signed Transaction"}},"required":["tx_hex"]})),
1453 ("http_fetch", serde_json::json!({"type":"object","properties":{"url":{"type":"string"},"cell_id":{"type":"string","description":"Cell that will consume the result (64-char hex)"}},"required":["url","cell_id"]})),
1454 ("get_sdk", serde_json::json!({"type":"object","properties":{},"required":[]})),
1455 ("faucet", serde_json::json!({"type":"object","properties":{"account_id":{"type":"string","description":"Your 64-char hex account ID"},"pubkey":{"type":"string","description":"Your ML-DSA public key hex"}},"required":["account_id","pubkey"]})),
1456 ].into_iter().collect();
1457
1458 let builtin_descs: std::collections::HashMap<&str, &str> = [
1459 ("get_chain_info", "Get current chain height, genesis hash, finalized height, and network info."),
1460 ("get_balance", "Get TRTH balance for an account. Input: account_id as 64-char hex."),
1461 ("get_validators", "List all active validators with stake and status."),
1462 ("get_token_info", "Get TRTH token metadata: name, symbol, decimals, total supply."),
1463 ("get_cell_info", "Get cell account info by cell_id as 64-char hex."),
1464 ("get_transaction", "Get transaction details by tx_hash as 64-char hex."),
1465 ("get_staking_info", "Get staking state: total staked, validator count, epoch info."),
1466 ("get_oracle_result", "Poll the result of an oracle HTTP fetch by request_id."),
1467 ("get_account_history", "Get recent transaction history for an account."),
1468 ("submit_transaction", "Submit a pre-signed transaction (hex-encoded bincode). Returns tx_hash."),
1469 ("http_fetch", "Queue an oracle HTTP GET. Validators fetch and commit-reveal the response. Returns request_id."),
1470 ("get_sdk", "Get Axiom CLI build and usage instructions. Axiom CLI generates ML-DSA-65 keypairs, signs transactions, and submits them to TruthLinked."),
1471 ("faucet", "Claim testnet TRTH tokens with Axiom CLI. Run axiom account-create first, then axiom faucet --from mykeys.json. 12-hour cooldown per account."),
1472 ].into_iter().collect();
1473
1474 let input_schema = builtin_schemas.get(name.as_str())
1475 .cloned()
1476 .unwrap_or_else(|| serde_json::json!({"type":"object"}));
1477 let description = builtin_descs.get(name.as_str())
1478 .map(|s| s.to_string())
1479 .unwrap_or_else(|| format!("On-chain tool cell {}.", &hex::encode(tool_id)[..8]));
1480
1481 Some(serde_json::json!({
1482 "name": name,
1483 "description": description,
1484 "inputSchema": input_schema,
1485 "_meta": {
1486 "cell_id": hex::encode(tool_id),
1487 "category": tool.storage.get(&tool_keys::CATEGORY).map(|b| b[0]).unwrap_or(0),
1488 "manifest_version": tool.manifest_version,
1489 "manifest_hash": hex::encode(tool.manifest_hash),
1490 "call_count": read_u64_from_storage(tool, &tool_keys::CALL_COUNT)
1491 }
1492 }))
1493 }).collect()
1494}
1495
1496pub fn enumerate_resources(
1497 state: &impl McpStateView,
1498 registry_id: &AccountId,
1499) -> Vec<serde_json::Value> {
1500 let registry = match state.cells().cells.get(registry_id) {
1501 Some(r) => r,
1502 None => return vec![],
1503 };
1504
1505 let count = read_u64_from_storage(registry, ®istry_keys::RESOURCE_COUNT);
1506
1507 (0..count).filter_map(|i| {
1508 let res_id = registry.storage.get(®istry_keys::resource_entry(i)).copied()?;
1509 let res = state.cells().cells.get(&res_id)?;
1510
1511 let name_bytes = res.storage.get(&resource_keys::NAME).copied().unwrap_or([0u8; 32]);
1512 let name = utf8_from_padded(&name_bytes);
1513
1514 let uri_bytes = res.storage.get(&resource_keys::URI_SCHEME).copied().unwrap_or([0u8; 32]);
1515 let uri_scheme = utf8_from_padded(&uri_bytes);
1516
1517 let mime_bytes = res.storage.get(&resource_keys::MIME_TYPE).copied().unwrap_or([0u8; 32]);
1518 let mime = utf8_from_padded(&mime_bytes);
1519
1520 Some(serde_json::json!({
1521 "uri": format!("trth://{}", hex::encode(res_id)),
1522 "name": name,
1523 "description": format!("On-chain resource cell. URI scheme: {}. Manifest v{}.", uri_scheme, res.manifest_version),
1524 "mimeType": mime
1525 }))
1526 }).collect()
1527}
1528
1529pub fn enumerate_prompts(
1530 state: &impl McpStateView,
1531 registry_id: &AccountId,
1532) -> Vec<serde_json::Value> {
1533 let registry = match state.cells().cells.get(registry_id) {
1534 Some(r) => r,
1535 None => return vec![],
1536 };
1537
1538 let count = read_u64_from_storage(registry, ®istry_keys::PROMPT_COUNT);
1539
1540 (0..count).filter_map(|i| {
1541 let prompt_id = registry.storage.get(®istry_keys::prompt_entry(i)).copied()?;
1542 let prompt = state.cells().cells.get(&prompt_id)?;
1543
1544 let approved_at = read_u64_from_storage(prompt, &prompt_keys::APPROVED_AT);
1545 if approved_at == 0 {
1546 return None;
1547 }
1548
1549 let name_bytes = prompt.storage.get(&prompt_keys::NAME).copied().unwrap_or([0u8; 32]);
1550 let name = utf8_from_padded(&name_bytes);
1551
1552 let arg_count = prompt.storage.get(&prompt_keys::ARG_COUNT)
1553 .map(|b| b[0] as u8).unwrap_or(0);
1554
1555 let arguments: Vec<serde_json::Value> = (0..arg_count).map(|j| {
1556 let arg_bytes = prompt.storage.get(&prompt_keys::arg_schema(j))
1557 .copied().unwrap_or([0u8; 32]);
1558 serde_json::json!({
1559 "name": utf8_from_padded(&arg_bytes),
1560 "required": true
1561 })
1562 }).collect();
1563
1564 Some(serde_json::json!({
1565 "name": name,
1566 "description": format!("On-chain prompt cell {}. Validator-approved. Manifest v{}.", &hex::encode(prompt_id)[..8], prompt.manifest_version),
1567 "arguments": arguments,
1568 "_meta": {
1569 "cell_id": hex::encode(prompt_id),
1570 "manifest_hash": hex::encode(prompt.manifest_hash),
1571 "use_count": read_u64_from_storage(prompt, &prompt_keys::USE_COUNT)
1572 }
1573 }))
1574 }).collect()
1575}
1576
1577pub fn read_resource(state: &impl McpStateView, registry_id: &AccountId, uri: &str) -> String {
1578 let path = uri.trim_start_matches("trth://");
1582 let parts: Vec<&str> = path.splitn(3, '/').collect();
1583
1584 let (cell_id_hex, slot_key_opt) = match parts.as_slice() {
1585 [cid] => (*cid, None),
1586 [cid, slot] => (*cid, Some(*slot)),
1587 ["name", name, slot] => {
1588 if let Some(registry) = state.cells().cells.get(registry_id) {
1590 let res_id_bytes = registry.storage.get(®istry_keys::name_to_resource(name));
1591 if let Some(id_bytes) = res_id_bytes {
1592 return read_resource(
1593 state,
1594 registry_id,
1595 &format!("trth://{}/{}", hex::encode(id_bytes), slot),
1596 );
1597 }
1598 }
1599 return serde_json::json!({ "error": "Resource name not found" }).to_string();
1600 }
1601 _ => return serde_json::json!({ "error": "Invalid URI format" }).to_string(),
1602 };
1603
1604 let cell_id_bytes = match hex::decode(cell_id_hex) {
1605 Ok(b) if b.len() == 32 => {
1606 let mut a = [0u8; 32];
1607 a.copy_from_slice(&b);
1608 a
1609 }
1610 _ => return serde_json::json!({ "error": "Invalid cell ID in URI" }).to_string(),
1611 };
1612
1613 let cell = match state.cells().cells.get(&cell_id_bytes) {
1614 Some(c) => c,
1615 None => return serde_json::json!({ "error": "Resource cell not found" }).to_string(),
1616 };
1617
1618 if let Some(slot_hex) = slot_key_opt {
1619 let slot_bytes = match hex::decode(slot_hex) {
1620 Ok(b) if b.len() == 32 => {
1621 let mut a = [0u8; 32];
1622 a.copy_from_slice(&b);
1623 a
1624 }
1625 Ok(b) => resource_keys::data_slot(&b),
1626 _ => return serde_json::json!({ "error": "Invalid slot key" }).to_string(),
1627 };
1628
1629 let value = cell
1630 .storage
1631 .get(&slot_bytes)
1632 .map(|v| hex::encode(v))
1633 .unwrap_or_else(|| {
1634 "0000000000000000000000000000000000000000000000000000000000000000".into()
1635 });
1636
1637 serde_json::json!({
1638 "cell_id": hex::encode(cell_id_bytes),
1639 "slot_key": hex::encode(slot_bytes),
1640 "value": value,
1641 "manifest_version": cell.manifest_version
1642 })
1643 .to_string()
1644 } else {
1645 let slots: HashMap<String, String> = cell
1647 .storage
1648 .iter()
1649 .take(256)
1650 .map(|(k, v)| (hex::encode(k), hex::encode(v)))
1651 .collect();
1652
1653 serde_json::json!({
1654 "cell_id": hex::encode(cell_id_bytes),
1655 "manifest_version": cell.manifest_version,
1656 "manifest_hash": hex::encode(cell.manifest_hash),
1657 "storage_slots": slots.len(),
1658 "storage": slots
1659 })
1660 .to_string()
1661 }
1662}
1663
1664pub fn get_prompt(
1665 state: &impl McpStateView,
1666 registry_id: &AccountId,
1667 name: &str,
1668) -> Option<serde_json::Value> {
1669 let registry = state.cells().cells.get(registry_id)?;
1670 let prompt_id = registry
1671 .storage
1672 .get(®istry_keys::name_to_prompt(name))
1673 .copied()?;
1674 let prompt = state.cells().cells.get(&prompt_id)?;
1675
1676 let template_hash = prompt
1677 .storage
1678 .get(&prompt_keys::TEMPLATE_HASH)
1679 .map(hex::encode)
1680 .unwrap_or_default();
1681
1682 let template_preview = if template_hash.len() >= 8 {
1683 &template_hash[..8]
1684 } else {
1685 &template_hash
1686 };
1687
1688 Some(serde_json::json!({
1689 "messages": [{
1690 "role": "user",
1691 "content": {
1692 "type": "text",
1693 "text": format!(
1694 "On-chain prompt '{}'. Template hash: {}. Fetch full template via: resources/read trth://{}/template",
1695 name, template_preview, hex::encode(prompt_id)
1696 )
1697 }
1698 }],
1699 "_meta": {
1700 "cell_id": hex::encode(prompt_id),
1701 "template_hash": template_hash,
1702 "manifest_version": prompt.manifest_version,
1703 "manifest_hash": hex::encode(prompt.manifest_hash)
1704 }
1705 }))
1706}
1707
1708pub mod protocol_addresses {
1715 pub fn mcp_registry() -> [u8; 32] {
1717 *blake3::hash(b"truthlinked:mcp:registry:v1").as_bytes()
1718 }
1719 pub fn agent_registry() -> [u8; 32] {
1721 *blake3::hash(b"truthlinked:mcp:agent_registry:v1").as_bytes()
1722 }
1723 pub fn action_log() -> [u8; 32] {
1725 *blake3::hash(b"truthlinked:mcp:action_log:v1").as_bytes()
1726 }
1727}
1728
1729pub fn deploy_mcp_genesis_cells(
1732 cell_state: &mut CellState,
1733 genesis_authority: AccountId,
1734 timestamp: u64,
1735) -> Result<(), String> {
1736 cell_state.deploy_cell(
1738 protocol_addresses::mcp_registry(),
1739 genesis_authority,
1740 vec![], {
1742 let mut m = HashMap::new();
1743 let zero = [0u8; 32];
1744 m.insert(registry_keys::TOOL_COUNT, zero);
1745 m.insert(registry_keys::RESOURCE_COUNT, zero);
1746 m.insert(registry_keys::PROMPT_COUNT, zero);
1747 m.insert(registry_keys::REGISTRY_VER, zero);
1748 m
1749 },
1750 0,
1751 timestamp,
1752 vec![
1753 registry_keys::TOOL_COUNT,
1754 registry_keys::RESOURCE_COUNT,
1755 registry_keys::PROMPT_COUNT,
1756 ],
1757 vec![
1758 registry_keys::TOOL_COUNT,
1759 registry_keys::RESOURCE_COUNT,
1760 registry_keys::PROMPT_COUNT,
1761 registry_keys::REGISTRY_VER,
1762 ],
1763 vec![], Vec::new(),
1765 Vec::new(),
1766 )?;
1767
1768 cell_state.deploy_cell(
1770 protocol_addresses::agent_registry(),
1771 genesis_authority,
1772 vec![],
1773 {
1774 let mut m = HashMap::new();
1775 m.insert(agent_reg_keys::AGENT_COUNT, [0u8; 32]);
1776 m
1777 },
1778 0,
1779 timestamp,
1780 vec![],
1781 vec![agent_reg_keys::AGENT_COUNT],
1782 vec![],
1783 Vec::new(),
1784 Vec::new(),
1785 )?;
1786
1787 cell_state.deploy_cell(
1789 protocol_addresses::action_log(),
1790 genesis_authority,
1791 vec![],
1792 HashMap::new(),
1793 0,
1794 timestamp,
1795 vec![],
1796 vec![],
1797 vec![], Vec::new(),
1800 Vec::new(),
1801 )?;
1802
1803 let builtin_tools: &[(&str, &str, u8, &str)] = &[
1805 ("get_chain_info", "Get current chain height, genesis hash, finalized height, and network info.", 0,
1806 r#"{"type":"object","properties":{},"required":[]}"#),
1807 ("get_balance", "Get TRTH balance for an account. Input: account_id as 64-char hex.", 0,
1808 r#"{"type":"object","properties":{"account_id":{"type":"string","description":"32-byte account ID as hex"}},"required":["account_id"]}"#),
1809 ("get_validators", "List all active validators with stake and status.", 0,
1810 r#"{"type":"object","properties":{},"required":[]}"#),
1811 ("get_token_info", "Get TRTH token metadata: name, symbol, decimals, total supply.", 0,
1812 r#"{"type":"object","properties":{},"required":[]}"#),
1813 ("get_cell_info", "Get cell account info by cell_id as 64-char hex.", 0,
1814 r#"{"type":"object","properties":{"cell_id":{"type":"string","description":"32-byte cell ID as hex"}},"required":["cell_id"]}"#),
1815 ("get_transaction", "Get transaction details by tx_hash as 64-char hex.", 0,
1816 r#"{"type":"object","properties":{"tx_hash":{"type":"string","description":"32-byte tx hash as hex"}},"required":["tx_hash"]}"#),
1817 ("get_staking_info", "Get staking state: total staked, validator count, epoch info.", 0,
1818 r#"{"type":"object","properties":{},"required":[]}"#),
1819 ("get_oracle_result", "Poll the result of an oracle HTTP fetch by request_id.", 0,
1820 r#"{"type":"object","properties":{"request_id":{"type":"string","description":"32-byte oracle request ID as hex"}},"required":["request_id"]}"#),
1821 ("get_account_history", "Get recent transaction history for an account.", 0,
1822 r#"{"type":"object","properties":{"account_id":{"type":"string"},"limit":{"type":"integer","default":20}},"required":["account_id"]}"#),
1823 ("submit_transaction", "Submit a pre-signed transaction (hex-encoded bincode). Returns tx_hash.", 1,
1824 r#"{"type":"object","properties":{"tx_hex":{"type":"string","description":"Hex-encoded bincode-serialized signed Transaction"}},"required":["tx_hex"]}"#),
1825 ("http_fetch", "Queue an oracle HTTP GET request. Validators fetch and commit-reveal the response. Returns request_id to poll with get_oracle_result.", 1,
1826 r#"{"type":"object","properties":{"url":{"type":"string","description":"URL to fetch"},"cell_id":{"type":"string","description":"Cell that will consume the result (64-char hex)"}},"required":["url","cell_id"]}"#),
1827 ("get_sdk", "Get Axiom CLI build and usage instructions. Axiom CLI generates ML-DSA-65 keypairs and signs transactions.", 0,
1828 r#"{"type":"object","properties":{},"required":[]}"#),
1829 ("faucet", "Claim testnet TRTH tokens. Requires account_id and pubkey from axiom account-create. 12-hour cooldown per account.", 0,
1830 r#"{"type":"object","properties":{"account_id":{"type":"string","description":"Your 64-char hex account ID"},"pubkey":{"type":"string","description":"Your Dilithium public key hex"}},"required":["account_id","pubkey"]}"#),
1831 ];
1832
1833 let registry_id = protocol_addresses::mcp_registry();
1834
1835 for (i, (name, _desc, category, schema_json)) in builtin_tools.iter().enumerate() {
1836 let tool_id =
1837 *blake3::hash(format!("truthlinked:mcp:builtin:{}", name).as_bytes()).as_bytes();
1838
1839 let schema_hash = *blake3::hash(schema_json.as_bytes()).as_bytes();
1840 let mut name_bytes = [0u8; 32];
1841 let n = name.len().min(32);
1842 name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
1843
1844 let mut cat_bytes = [0u8; 32];
1845 cat_bytes[0] = *category;
1846 let mut enabled = [0u8; 32];
1847 enabled[0] = 1;
1848 let mut count_bytes = [0u8; 32];
1849 count_bytes[..8].copy_from_slice(&(i as u64).to_le_bytes());
1850
1851 let mut tool_storage = HashMap::new();
1852 tool_storage.insert(tool_keys::NAME, name_bytes);
1853 tool_storage.insert(tool_keys::SCHEMA_HASH, schema_hash);
1854 tool_storage.insert(tool_keys::DESC_HASH, schema_hash);
1855 tool_storage.insert(tool_keys::CATEGORY, cat_bytes);
1856 tool_storage.insert(tool_keys::ENABLED, enabled);
1857 tool_storage.insert(tool_keys::OWNER, genesis_authority);
1858 tool_storage.insert(tool_keys::CALL_COUNT, [0u8; 32]);
1859
1860 cell_state.deploy_cell(
1861 tool_id,
1862 genesis_authority,
1863 vec![], tool_storage,
1865 0,
1866 timestamp,
1867 vec![],
1868 vec![tool_keys::CALL_COUNT],
1869 vec![tool_keys::CALL_COUNT], Vec::new(),
1871 Vec::new(),
1872 )?;
1873
1874 let idx = i as u64;
1876 let mut new_count = [0u8; 32];
1877 new_count[..8].copy_from_slice(&(idx + 1).to_le_bytes());
1878 let mut ver_bytes = [0u8; 32];
1879 ver_bytes[..8].copy_from_slice(&(idx + 1).to_le_bytes());
1880
1881 let registry = cell_state
1882 .cells
1883 .get_mut(®istry_id)
1884 .ok_or("McpRegistry cell missing")?;
1885 registry
1886 .storage
1887 .insert(registry_keys::tool_entry(idx), tool_id);
1888 registry
1889 .storage
1890 .insert(registry_keys::name_to_tool(name), tool_id);
1891 registry
1892 .storage
1893 .insert(registry_keys::TOOL_COUNT, new_count);
1894 registry
1895 .storage
1896 .insert(registry_keys::REGISTRY_VER, ver_bytes);
1897 }
1898
1899 tracing::info!(
1900 " MCP protocol cells deployed: registry={} agent_reg={} action_log={} builtin_tools={}",
1901 hex::encode(protocol_addresses::mcp_registry()),
1902 hex::encode(protocol_addresses::agent_registry()),
1903 hex::encode(protocol_addresses::action_log()),
1904 builtin_tools.len(),
1905 );
1906
1907 Ok(())
1908}
1909
1910fn read_u64_from_storage(cell: &CellAccount, key: &[u8; 32]) -> u64 {
1915 cell.storage
1916 .get(key)
1917 .map(|b| u64::from_le_bytes(b[..8].try_into().unwrap_or([0u8; 8])))
1918 .unwrap_or(0)
1919}
1920
1921fn utf8_from_padded(bytes: &[u8; 32]) -> String {
1922 String::from_utf8_lossy(bytes.split(|&b| b == 0).next().unwrap_or(&[])).to_string()
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927 use super::*;
1928
1929 struct TestState {
1930 cells: CellState,
1931 accounts: ImHashMap<AccountId, AccountRecord>,
1932 params: ImHashMap<[u8; 32], [u8; 32]>,
1933 }
1934
1935 impl McpStateView for TestState {
1936 fn cells(&self) -> &CellState {
1937 &self.cells
1938 }
1939
1940 fn accounts(&self) -> &ImHashMap<AccountId, AccountRecord> {
1941 &self.accounts
1942 }
1943 }
1944
1945 impl truthlinked_governance::params::ParamState for TestState {
1946 fn params(&self) -> &ImHashMap<[u8; 32], [u8; 32]> {
1947 &self.params
1948 }
1949
1950 fn params_mut(&mut self) -> &mut ImHashMap<[u8; 32], [u8; 32]> {
1951 &mut self.params
1952 }
1953 }
1954
1955 fn setup_state_with_registry() -> TestState {
1956 let mut state = TestState {
1957 cells: CellState::new(),
1958 accounts: ImHashMap::new(),
1959 params: ImHashMap::new(),
1960 };
1961 truthlinked_governance::params::insert_genesis_params(&mut state);
1962 truthlinked_governance::params::rehydrate_from_state(&state);
1963
1964 let genesis_authority = [1u8; 32];
1965 deploy_mcp_genesis_cells(&mut state.cells, genesis_authority, 0).unwrap();
1966 assert!(
1967 truthlinked_governance::params::get_param_by_key(
1968 &truthlinked_governance::params::param_key(
1969 truthlinked_governance::params::PARAM_MAX_CELL_BYTECODE_SIZE,
1970 )
1971 )
1972 .is_some(),
1973 "MCP tests require max cell bytecode genesis param"
1974 );
1975 state
1976 }
1977
1978 #[test]
1979 fn register_tool_rejects_invalid_bytecode() {
1980 let mut state = setup_state_with_registry();
1981 state.accounts.insert(
1982 [9u8; 32],
1983 AccountRecord {
1984 pubkey_bytes: vec![],
1985 balance: gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE) * 2,
1986 compute_escrow_trth: 0,
1987 nonce: 0,
1988 nfts: vec![],
1989 },
1990 );
1991 let intent = McpIntent::RegisterMcpTool {
1992 tool_id: [2u8; 32],
1993 bytecode: vec![1, 2, 3], name: "tool".into(),
1995 input_schema_json: b"{}".to_vec(),
1996 category: 0,
1997 declared_reads: vec![],
1998 declared_writes: vec![],
1999 commutative_keys: vec![],
2000 oracle_schema_ids: vec![],
2001 registry_id: protocol_addresses::mcp_registry(),
2002 };
2003 let err = diff_register_tool(&state, [9u8; 32], &intent, 0).unwrap_err();
2004 assert!(
2005 err.contains("Invalid Axiom bytecode") || err.contains("Invalid bytecode"),
2006 "got: {err}"
2007 );
2008 }
2009
2010 #[test]
2011 fn enumerate_tools_hides_custom_registered_tools() {
2012 let mut state = setup_state_with_registry();
2013 let registry_id = protocol_addresses::mcp_registry();
2014 let custom_tool_id = [7u8; 32];
2015
2016 let mut name_bytes = [0u8; 32];
2017 name_bytes[.."custom-tool".len()].copy_from_slice(b"custom-tool");
2018 let mut category = [0u8; 32];
2019 category[0] = 1;
2020 let mut enabled = [0u8; 32];
2021 enabled[0] = 1;
2022 let mut storage = HashMap::new();
2023 storage.insert(tool_keys::NAME, name_bytes);
2024 storage.insert(tool_keys::CATEGORY, category);
2025 storage.insert(tool_keys::ENABLED, enabled);
2026 storage.insert(tool_keys::SCHEMA_HASH, [0u8; 32]);
2027 storage.insert(tool_keys::DESC_HASH, [0u8; 32]);
2028 storage.insert(tool_keys::OWNER, [1u8; 32]);
2029 storage.insert(tool_keys::CALL_COUNT, [0u8; 32]);
2030
2031 state
2032 .cells
2033 .deploy_cell(
2034 custom_tool_id,
2035 [1u8; 32],
2036 vec![],
2037 storage,
2038 0,
2039 0,
2040 vec![],
2041 vec![tool_keys::CALL_COUNT],
2042 vec![tool_keys::CALL_COUNT],
2043 Vec::new(),
2044 Vec::new(),
2045 )
2046 .unwrap();
2047
2048 let registry = state.cells.cells.get_mut(®istry_id).unwrap();
2049 let count = read_u64_from_storage(registry, ®istry_keys::TOOL_COUNT);
2050 registry
2051 .storage
2052 .insert(registry_keys::tool_entry(count), custom_tool_id);
2053 let mut new_count = [0u8; 32];
2054 new_count[..8].copy_from_slice(&(count + 1).to_le_bytes());
2055 registry
2056 .storage
2057 .insert(registry_keys::TOOL_COUNT, new_count);
2058
2059 let tools = enumerate_tools(
2060 &state,
2061 ®istry_id,
2062 &protocol_addresses::agent_registry(),
2063 &[0u8; 32],
2064 );
2065 assert!(tools
2066 .iter()
2067 .any(|tool| tool.get("name").and_then(|v| v.as_str()) == Some("get_chain_info")));
2068 assert!(!tools
2069 .iter()
2070 .any(|tool| tool.get("name").and_then(|v| v.as_str()) == Some("custom-tool")));
2071 }
2072
2073 #[test]
2074 fn register_resource_rejects_large_initial_data() {
2075 let mut state = setup_state_with_registry();
2076 state.accounts.insert(
2077 [9u8; 32],
2078 AccountRecord {
2079 pubkey_bytes: vec![],
2080 balance: gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE) * 2,
2081 compute_escrow_trth: 0,
2082 nonce: 0,
2083 nfts: vec![],
2084 },
2085 );
2086 let intent = McpIntent::RegisterMcpResource {
2087 resource_id: [3u8; 32],
2088 bytecode: vec![],
2089 name: "res".into(),
2090 uri_scheme: "trth".into(),
2091 mime_type: "application/json".into(),
2092 initial_data: vec![(b"slot".to_vec(), vec![0u8; 33])],
2093 declared_reads: vec![],
2094 declared_writes: vec![],
2095 oracle_schema_ids: vec![],
2096 registry_id: protocol_addresses::mcp_registry(),
2097 };
2098 let err = diff_register_resource(&state, [9u8; 32], &intent, 0).unwrap_err();
2099 assert!(err.contains("<= 32 bytes"));
2100 }
2101}