diff --git a/interfaces/sourcegen/sourcegen/_TagFileParser.py b/interfaces/sourcegen/sourcegen/_TagFileParser.py
index 46b2614e9c..d3e5c890d8 100644
--- a/interfaces/sourcegen/sourcegen/_TagFileParser.py
+++ b/interfaces/sourcegen/sourcegen/_TagFileParser.py
@@ -69,6 +69,16 @@ def qualified_name(self):
         return self.name
 
 
+@dataclass(frozen=True)
+@with_unpack_iter
+class TagDetails(TagInfo):
+    """Create tag information based on XML data."""
+
+    location: str = ""  #: file containing doxygen description
+    briefdescription: str = ""  #: brief doxygen description
+    parameterlist: list[Param] = None  #: annotated doxygen parameter list
+
+
 class TagFileParser:
     """Class handling contents of doxygen tag file."""
 
@@ -101,9 +111,8 @@ def xml_compounds(kind: str, names: list[str]) -> dict[str,str]:
         unknown = set(bases) - set(class_names)
         if unknown:
             unknown = "', '".join(unknown)
-            _logger.warning(
-                "Class(es) in configuration file are missing from tag file: '%s'",
-                unknown)
+            _logger.warning("Class(es) in configuration file are missing "
+                            f"from tag file: {unknown!r}")
 
         # Parse content of classes that are specified by the configuration file
         class_names = set(bases) & set(class_names)
@@ -144,7 +153,7 @@ def tag_info(self, func_string: str) -> TagInfo:
         """Look up tag information based on (partial) function signature."""
         cxx_func = func_string.split("(")[0].split(" ")[-1]
         if cxx_func not in self._known:
-            _logger.critical(f"Did not find {cxx_func!r} in doxygen tag file.")
+            _logger.critical(f"Could not find {cxx_func!r} in doxygen tag file.")
             sys.exit(1)
         ix = 0
         if len(self._known[cxx_func]) > 1:
@@ -174,28 +183,97 @@ def tag_info(self, func_string: str) -> TagInfo:
         return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix])
 
 
-def xml_tag(tag: str, text: str, suffix: str="", index=0) -> str:
-    """Extract content enclosed between XML tags, optionally skipping matches."""
-    if suffix:
-        suffix = f" {suffix.strip()}"
-    regex = re.compile(rf'(?<=<{tag}{suffix}>)(.*?)(?=</{tag}>)', flags=re.DOTALL)
-    match = re.findall(regex, text)
-    if index >= len(match):
-        return ""  # not enough matches found
-    return match[index].strip()
-
-
-def xml_tags(tag: str, text: str, suffix: str="") -> list[str]:
+def tag_lookup(tag_info: TagInfo) -> TagDetails:
+    """Retrieve tag details from doxygen tree."""
+    xml_file = _xml_path / tag_info.anchorfile
+    if not xml_file.exists():
+        msg = (f"XML file does not exist at expected location: {xml_file}")
+        _logger.error(msg)
+        return TagDetails()
+
+    with xml_file.open() as fid:
+        xml_details = fid.read()
+
+    id_ = tag_info.id
+    regex = re.compile(rf'<memberdef kind="function" id="{id_}"[\s\S]*?</memberdef>')
+    matches = re.findall(regex, xml_details)
+
+    if not matches:
+        _logger.error(f"No XML matches found for {tag_info.qualified_name!r}")
+        return TagDetails()
+    if len(matches) != 1:
+        _logger.error(f"Inconclusive XML matches found for {tag_info.qualified_name!r}")
+        return TagDetails()
+
+    def cleanup(entry: str) -> str:
+        # Remove stray XML markup
+        if entry.startswith("<para>"):
+            entry = xml_tag("para", entry)
+        if "<ref" in entry:
+            regex = re.compile(r'<ref [\s\S]*?>')
+            for ref in re.findall(regex, entry):
+                entry = entry.replace(ref, "<ref>")
+            entry = entry.replace("<ref>", "").replace("</ref>", "")
+        return entry
+
+    def resolve_parameteritem(par_map: str) -> list[Param]:
+        # Resolve/flatten parameter list
+        name_lines = xml_tag("parameternamelist", par_map).split("\n")
+        regex = re.compile(r'(?<=<parametername)(.*?)(?=>)')
+        names = []
+        directions = []
+        for name_line in name_lines:
+            direction = re.findall(regex, name_line)[0]
+            if "=" in direction:
+                name_line = name_line.replace(direction, "")
+                direction = direction.split("=")[1].strip('"')
+            directions.append(direction)
+            names.append(xml_tag("parametername", name_line))
+        description = cleanup(xml_tag("parameterdescription", par_map))
+        return [Param("", n, description, d) for n, d in zip(names, directions)]
+
+    xml = matches[0]
+    par_list = []
+    par_block = xml_tag("parameterlist", xml, suffix='kind="param"')
+    if par_block:
+        for par_map in xml_tags("parameteritem", par_block):
+            par_list.extend(resolve_parameteritem(par_map))
+
+    def xml_attribute(attr: str, text: str, *, entry: str) -> str:
+        """Extract XML attribute."""
+        regex = re.compile(rf'(?<=<{attr} )(.*?)(?=/>)', flags=re.DOTALL)
+        match = re.findall(regex, text)
+        if not match:
+            return ""  # not enough matches found
+        entries = dict([tuple(_.split("=")) for _ in match[0].strip().split(" ")])
+        return entries.get(entry, "").strip('"')
+
+    return TagDetails(*tag_info,
+                      xml_attribute("location", xml, entry="file"),
+                      cleanup(xml_tag("briefdescription", xml)),
+                      par_list)
+
+
+def xml_tag(tag: str, text: str, suffix: str="") -> str:
+    """Extract first match of content enclosed between XML tags."""
+    match = xml_tags(tag, text, suffix, True)
+    if match:
+        return match[0].strip()
+    return ""  # not enough matches found
+
+
+def xml_tags(tag: str, text: str, suffix: str="", permissive: bool=False) -> list[str]:
     """Extract list of content enclosed by XML tags."""
     if suffix:
         suffix = f" {suffix.strip()}"
     regex = re.compile(rf'(?<=<{tag}{suffix}>)(.*?)(?=</{tag}>)',
                        flags=re.DOTALL|re.MULTILINE)
     matched = re.findall(regex, text)
-    if not matched:
-        blanks = text.split("\n")[-1].split("<")[0]
-        msg = f"Could not extract {tag!r} from:\n{blanks}{text}\n"
-        msg += f"using regex: {regex}"
-        _logger.error(msg)
-        return []
-    return matched
+    if matched or permissive:
+        return matched
+
+    blanks = text.split("\n")[-1].split("<")[0]
+    msg = f"Could not extract {tag!r} from:\n{blanks}{text}\n"
+    msg += f"using regex: {regex}"
+    _logger.error(msg)
+    return []
diff --git a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py
index 8deec3f1c3..559709ec54 100644
--- a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py
+++ b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py
@@ -11,9 +11,9 @@
 
 from ._Config import Config
 
-from .._dataclasses import HeaderFile, Recipe #ArgList, Param, Func
+from .._dataclasses import HeaderFile, Recipe, Param #ArgList, Param, Func
 from .._SourceGenerator import SourceGenerator
-from .._TagFileParser import TagFileParser #, TagDetails, tag_lookup
+from .._TagFileParser import TagFileParser, TagDetails, tag_lookup
 
 
 _logger = logging.getLogger()
@@ -57,11 +57,11 @@ def build_declaration(self, recipe: Recipe) -> tuple[str, str, list[str]]:
 
         if recipe.implements:
             tag_info = self._doxygen_tags.tag_info(recipe.implements)
-            _logger.info(f"    recipe for {recipe.name!r}: {tag_info.anchorfile}")
-            # details = tag_lookup(tag_info)
+            details = tag_lookup(tag_info)
 
-        #     # convert XML return type to format suitable for crosswalk
-        #     ret_type = Param.from_xml(details.type).p_type
+            # convert XML return type to format suitable for crosswalk
+            ret_type = Param.from_xml(details.type).p_type
+            _logger.info(f"    recipe for {recipe.name!r}: {details.qualified_name}")
         #     ret_param, buffer_params, cabinets = self._ret_crosswalk(ret_type)
         #     par_list = merge_params(recipe.implements, details)
         #     prop_params, prop_cabinets = self._prop_crosswalk(par_list)
@@ -69,10 +69,10 @@ def build_declaration(self, recipe: Recipe) -> tuple[str, str, list[str]]:
         #     args = prop_params + buffer_params
 
         elif recipe.what == "destructor":
-            _logger.info(f"    recipe for {recipe.name!r}: destructor")
-        #     args = [Param("int", "handle", f"Handle to {recipe.base} object.")]
-        #     details = TagDetails(
-        #         "", "", "", "", "", "", f"Delete {recipe.base} object.", "", args)
+            args = [Param("int", "handle", f"Handle to {recipe.base} object.")]
+            details = TagDetails(
+                "", "", "", "", "", "", f"Delete {recipe.base} object.", "", args)
+            _logger.info(f"    recipe for {recipe.name!r}: void")
         #     ret_param = Param(
         #         "int", "", "Zero for success and -1 for exception handling.")
         #     annotations = f"//! {details.briefdescription}"